mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-23 10:16:32 -05:00
Add renderer ready state to global application state
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
2dad1d565e
commit
cfb2de8909
12 changed files with 144 additions and 52 deletions
|
@ -10,6 +10,7 @@ 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'
|
||||
|
||||
/**
|
||||
* Creates a new info line for the document information dialog that holds the
|
||||
|
@ -19,17 +20,23 @@ export const DocumentInfoLineWordCount: React.FC = () => {
|
|||
useTranslation()
|
||||
const iframeEditorToRendererCommunicator = useIFrameEditorToRendererCommunicator()
|
||||
const [wordCount, setWordCount] = useState<number | null>(null)
|
||||
const rendererReady = useApplicationState((state) => state.editorConfig.rendererReady)
|
||||
|
||||
useEffect(() => {
|
||||
iframeEditorToRendererCommunicator?.onWordCountCalculated((words) => {
|
||||
iframeEditorToRendererCommunicator.onWordCountCalculated((words) => {
|
||||
setWordCount(words)
|
||||
})
|
||||
iframeEditorToRendererCommunicator?.sendGetWordCount()
|
||||
return () => {
|
||||
iframeEditorToRendererCommunicator?.onWordCountCalculated(undefined)
|
||||
iframeEditorToRendererCommunicator.onWordCountCalculated(undefined)
|
||||
}
|
||||
}, [iframeEditorToRendererCommunicator, setWordCount])
|
||||
|
||||
useEffect(() => {
|
||||
if (rendererReady) {
|
||||
iframeEditorToRendererCommunicator.sendGetWordCount()
|
||||
}
|
||||
}, [iframeEditorToRendererCommunicator, rendererReady])
|
||||
|
||||
return (
|
||||
<DocumentInfoLine icon={'align-left'} size={'2x'}>
|
||||
<ShowIf condition={wordCount === null}>
|
||||
|
|
|
@ -31,7 +31,7 @@ export const IframeRendererToEditorCommunicatorContextProvider: React.FC = ({ ch
|
|||
const editorOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.editorOrigin)
|
||||
const currentIFrameCommunicator = useMemo<IframeRendererToEditorCommunicator>(() => {
|
||||
const newCommunicator = new IframeRendererToEditorCommunicator()
|
||||
newCommunicator.setOtherSide(window.parent, editorOrigin)
|
||||
newCommunicator.setMessageTarget(window.parent, editorOrigin)
|
||||
return newCommunicator
|
||||
}, [editorOrigin])
|
||||
|
||||
|
|
|
@ -19,12 +19,12 @@ export const useOnIframeLoad = (
|
|||
return useCallback(() => {
|
||||
const frame = frameReference.current
|
||||
if (!frame || !frame.contentWindow) {
|
||||
iframeCommunicator.unsetOtherSide()
|
||||
iframeCommunicator.unsetMessageTarget()
|
||||
return
|
||||
}
|
||||
|
||||
if (sendToRenderPage.current) {
|
||||
iframeCommunicator.setOtherSide(frame.contentWindow, rendererOrigin)
|
||||
iframeCommunicator.setMessageTarget(frame.contentWindow, rendererOrigin)
|
||||
sendToRenderPage.current = false
|
||||
return
|
||||
} else {
|
||||
|
|
|
@ -7,6 +7,7 @@ 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 { setRendererReady } from '../../../redux/editor/methods'
|
||||
import { isTestMode } from '../../../utils/test-modes'
|
||||
import { RendererProps } from '../../render-page/markdown-document'
|
||||
import { ImageDetails, RendererType } from '../../render-page/rendering-message'
|
||||
|
@ -16,7 +17,6 @@ import { useOnIframeLoad } from './hooks/use-on-iframe-load'
|
|||
import { ShowOnPropChangeImageLightbox } from './show-on-prop-change-image-lightbox'
|
||||
|
||||
export interface RenderIframeProps extends RendererProps {
|
||||
onRendererReadyChange?: (rendererReady: boolean) => void
|
||||
rendererType: RendererType
|
||||
forcedDarkMode?: boolean
|
||||
frameClasses?: string
|
||||
|
@ -31,13 +31,11 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
|||
onScroll,
|
||||
onMakeScrollSource,
|
||||
frameClasses,
|
||||
onRendererReadyChange,
|
||||
rendererType,
|
||||
forcedDarkMode
|
||||
}) => {
|
||||
const savedDarkMode = useIsDarkModeActivated()
|
||||
const darkMode = forcedDarkMode ?? savedDarkMode
|
||||
const [rendererReady, setRendererReady] = useState<boolean>(false)
|
||||
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
|
||||
|
||||
const frameReference = useRef<HTMLIFrameElement>(null)
|
||||
|
@ -54,29 +52,51 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
|||
)
|
||||
const [frameHeight, setFrameHeight] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
onRendererReadyChange?.(rendererReady)
|
||||
}, [onRendererReadyChange, rendererReady])
|
||||
const rendererReady = useApplicationState((state) => state.editorConfig.rendererReady)
|
||||
|
||||
useEffect(() => () => iframeCommunicator.unregisterEventListener(), [iframeCommunicator])
|
||||
useEffect(
|
||||
() => iframeCommunicator.onFirstHeadingChange(onFirstHeadingChange),
|
||||
[iframeCommunicator, onFirstHeadingChange]
|
||||
() => () => {
|
||||
iframeCommunicator.unregisterEventListener()
|
||||
setRendererReady(false)
|
||||
},
|
||||
[iframeCommunicator]
|
||||
)
|
||||
useEffect(
|
||||
() => iframeCommunicator.onFrontmatterChange(onFrontmatterChange),
|
||||
[iframeCommunicator, onFrontmatterChange]
|
||||
)
|
||||
useEffect(() => iframeCommunicator.onSetScrollState(onScroll), [iframeCommunicator, onScroll])
|
||||
useEffect(
|
||||
() => iframeCommunicator.onSetScrollSourceToRenderer(onMakeScrollSource),
|
||||
[iframeCommunicator, onMakeScrollSource]
|
||||
)
|
||||
useEffect(
|
||||
() => iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange),
|
||||
[iframeCommunicator, onTaskCheckedChange]
|
||||
)
|
||||
useEffect(() => iframeCommunicator.onImageClicked(setLightboxDetails), [iframeCommunicator])
|
||||
|
||||
useEffect(() => {
|
||||
iframeCommunicator.onFirstHeadingChange(onFirstHeadingChange)
|
||||
return () => iframeCommunicator.onFirstHeadingChange(undefined)
|
||||
}, [iframeCommunicator, onFirstHeadingChange])
|
||||
|
||||
useEffect(() => {
|
||||
iframeCommunicator.onFrontmatterChange(onFrontmatterChange)
|
||||
return () => iframeCommunicator.onFrontmatterChange(undefined)
|
||||
}, [iframeCommunicator, onFrontmatterChange])
|
||||
|
||||
useEffect(() => {
|
||||
iframeCommunicator.onSetScrollState(onScroll)
|
||||
return () => iframeCommunicator.onSetScrollState(undefined)
|
||||
}, [iframeCommunicator, onScroll])
|
||||
|
||||
useEffect(() => {
|
||||
iframeCommunicator.onSetScrollSourceToRenderer(onMakeScrollSource)
|
||||
return () => iframeCommunicator.onSetScrollSourceToRenderer(undefined)
|
||||
}, [iframeCommunicator, onMakeScrollSource])
|
||||
|
||||
useEffect(() => {
|
||||
iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange)
|
||||
return () => iframeCommunicator.onTaskCheckboxChange(undefined)
|
||||
}, [iframeCommunicator, onTaskCheckedChange])
|
||||
|
||||
useEffect(() => {
|
||||
iframeCommunicator.onImageClicked(setLightboxDetails)
|
||||
return () => iframeCommunicator.onImageClicked(undefined)
|
||||
}, [iframeCommunicator])
|
||||
|
||||
useEffect(() => {
|
||||
iframeCommunicator.onHeightChange(setFrameHeight)
|
||||
return () => iframeCommunicator.onHeightChange(undefined)
|
||||
}, [iframeCommunicator])
|
||||
|
||||
useEffect(() => {
|
||||
iframeCommunicator.onRendererReady(() => {
|
||||
iframeCommunicator.sendSetBaseConfiguration({
|
||||
|
@ -85,8 +105,8 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
|||
})
|
||||
setRendererReady(true)
|
||||
})
|
||||
}, [darkMode, rendererType, iframeCommunicator, rendererReady, scrollState])
|
||||
useEffect(() => iframeCommunicator.onHeightChange(setFrameHeight), [iframeCommunicator])
|
||||
return () => iframeCommunicator.onRendererReady(undefined)
|
||||
}, [iframeCommunicator, rendererType])
|
||||
|
||||
useEffect(() => {
|
||||
if (rendererReady) {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { Branding } from '../common/branding/branding'
|
||||
import {
|
||||
|
@ -20,10 +20,11 @@ import { ShowIf } from '../common/show-if/show-if'
|
|||
import { RendererType } from '../render-page/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'
|
||||
|
||||
export const IntroPage: React.FC = () => {
|
||||
const introPageContent = useIntroPageContent()
|
||||
const [rendererReady, setRendererReady] = useState<boolean>(true)
|
||||
const rendererReady = useApplicationState((state) => state.editorConfig.rendererReady)
|
||||
|
||||
return (
|
||||
<IframeEditorToRendererCommunicatorContextProvider>
|
||||
|
@ -45,7 +46,6 @@ export const IntroPage: React.FC = () => {
|
|||
<RenderIframe
|
||||
frameClasses={'w-100 overflow-y-hidden'}
|
||||
markdownContent={introPageContent as string}
|
||||
onRendererReadyChange={setRendererReady}
|
||||
rendererType={RendererType.INTRO}
|
||||
forcedDarkMode={true}
|
||||
/>
|
||||
|
|
|
@ -4,38 +4,74 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* Error that will be thrown if a message couldn't be sent.
|
||||
*/
|
||||
export class IframeCommunicatorSendingError extends Error {}
|
||||
|
||||
/**
|
||||
* Base class for communication between renderer and editor.
|
||||
*/
|
||||
export abstract class IframeCommunicator<SEND, RECEIVE> {
|
||||
protected otherSide?: Window
|
||||
protected otherOrigin?: string
|
||||
private messageTarget?: Window
|
||||
private targetOrigin?: string
|
||||
private communicationEnabled: boolean
|
||||
|
||||
constructor() {
|
||||
window.addEventListener('message', this.handleEvent.bind(this))
|
||||
this.communicationEnabled = false
|
||||
}
|
||||
|
||||
public unregisterEventListener(): void {
|
||||
window.removeEventListener('message', this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
public setOtherSide(otherSide: Window, otherOrigin: string): void {
|
||||
this.otherSide = otherSide
|
||||
this.otherOrigin = otherOrigin
|
||||
/**
|
||||
* Sets the target for message sending.
|
||||
* Messages can be sent as soon as the communication is enabled.
|
||||
*
|
||||
* @see enableCommunication
|
||||
* @param otherSide The target {@link Window} that should receive the messages.
|
||||
* @param otherOrigin The origin from the URL of the target. If this isn't correct then the message sending will produce CORS errors.
|
||||
*/
|
||||
public setMessageTarget(otherSide: Window, otherOrigin: string): void {
|
||||
this.messageTarget = otherSide
|
||||
this.targetOrigin = otherOrigin
|
||||
this.communicationEnabled = false
|
||||
}
|
||||
|
||||
public unsetOtherSide(): void {
|
||||
this.otherSide = undefined
|
||||
this.otherOrigin = undefined
|
||||
/**
|
||||
* Unsets the message target. Should be used if the old target isn't available anymore.
|
||||
*/
|
||||
public unsetMessageTarget(): void {
|
||||
this.messageTarget = undefined
|
||||
this.targetOrigin = undefined
|
||||
this.communicationEnabled = false
|
||||
}
|
||||
|
||||
public getOtherSide(): Window | undefined {
|
||||
return this.otherSide
|
||||
/**
|
||||
* Enables the message communication.
|
||||
* Should be called as soon as the other sides is ready to receive messages.
|
||||
*/
|
||||
protected enableCommunication(): void {
|
||||
this.communicationEnabled = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the message target.
|
||||
*
|
||||
* @param message The message to send.
|
||||
*/
|
||||
protected sendMessageToOtherSide(message: SEND): void {
|
||||
if (this.otherSide === undefined || this.otherOrigin === undefined) {
|
||||
console.error("Can't send message because otherSide is null", message)
|
||||
return
|
||||
if (this.messageTarget === undefined || this.targetOrigin === undefined) {
|
||||
throw new IframeCommunicatorSendingError(`Other side is not set.\nMessage was: ${JSON.stringify(message)}`)
|
||||
}
|
||||
this.otherSide.postMessage(message, this.otherOrigin)
|
||||
if (!this.communicationEnabled) {
|
||||
throw new IframeCommunicatorSendingError(
|
||||
`Communication isn't enabled. Maybe the other side is not ready?\nMessage was: ${JSON.stringify(message)}`
|
||||
)
|
||||
}
|
||||
this.messageTarget.postMessage(message, this.targetOrigin)
|
||||
}
|
||||
|
||||
protected abstract handleEvent(event: MessageEvent<RECEIVE>): void
|
||||
|
|
|
@ -106,6 +106,7 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<
|
|||
const renderMessage = event.data
|
||||
switch (renderMessage.type) {
|
||||
case RenderIframeMessageType.RENDERER_READY:
|
||||
this.enableCommunication()
|
||||
this.onRendererReadyHandler?.()
|
||||
return false
|
||||
case RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER:
|
||||
|
|
|
@ -38,7 +38,7 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
|||
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
|
||||
useEffect(
|
||||
() => iframeCommunicator?.onGetWordCount(countWordsInRenderedDocument),
|
||||
() => iframeCommunicator.onGetWordCount(countWordsInRenderedDocument),
|
||||
[iframeCommunicator, countWordsInRenderedDocument]
|
||||
)
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<
|
|||
}
|
||||
|
||||
public sendRendererReady(): void {
|
||||
this.enableCommunication()
|
||||
this.sendMessageToOtherSide({
|
||||
type: RenderIframeMessageType.RENDERER_READY
|
||||
})
|
||||
|
|
|
@ -14,7 +14,8 @@ import {
|
|||
SetEditorLigaturesAction,
|
||||
SetEditorPreferencesAction,
|
||||
SetEditorSmartPasteAction,
|
||||
SetEditorSyncScrollAction
|
||||
SetEditorSyncScrollAction,
|
||||
SetRendererReadyAction
|
||||
} from './types'
|
||||
|
||||
export const loadFromLocalStorage = (): EditorConfig | undefined => {
|
||||
|
@ -46,6 +47,19 @@ export const setEditorMode = (editorMode: EditorMode): void => {
|
|||
store.dispatch(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a global application state change for the "renderer ready" state.
|
||||
*
|
||||
* @param rendererReady The new renderer ready state.
|
||||
*/
|
||||
export const setRendererReady = (rendererReady: boolean): void => {
|
||||
const action: SetRendererReadyAction = {
|
||||
type: EditorConfigActionType.SET_RENDERER_READY,
|
||||
rendererReady
|
||||
}
|
||||
store.dispatch(action)
|
||||
}
|
||||
|
||||
export const setEditorSyncScroll = (syncScroll: boolean): void => {
|
||||
const action: SetEditorSyncScrollAction = {
|
||||
type: EditorConfigActionType.SET_SYNC_SCROLL,
|
||||
|
|
|
@ -15,7 +15,8 @@ import {
|
|||
SetEditorLigaturesAction,
|
||||
SetEditorPreferencesAction,
|
||||
SetEditorSmartPasteAction,
|
||||
SetEditorSyncScrollAction
|
||||
SetEditorSyncScrollAction,
|
||||
SetRendererReadyAction
|
||||
} from './types'
|
||||
|
||||
const initialState: EditorConfig = {
|
||||
|
@ -23,6 +24,7 @@ const initialState: EditorConfig = {
|
|||
ligatures: true,
|
||||
syncScroll: true,
|
||||
smartPaste: true,
|
||||
rendererReady: false,
|
||||
preferences: {
|
||||
theme: 'one-dark',
|
||||
keyMap: 'sublime',
|
||||
|
@ -55,6 +57,11 @@ export const EditorConfigReducer: Reducer<EditorConfig, EditorConfigActions> = (
|
|||
}
|
||||
saveToLocalStorage(newState)
|
||||
return newState
|
||||
case EditorConfigActionType.SET_RENDERER_READY:
|
||||
return {
|
||||
...state,
|
||||
rendererReady: (action as SetRendererReadyAction).rendererReady
|
||||
}
|
||||
case EditorConfigActionType.SET_LIGATURES:
|
||||
newState = {
|
||||
...state,
|
||||
|
|
|
@ -13,7 +13,8 @@ export enum EditorConfigActionType {
|
|||
SET_SYNC_SCROLL = 'editor/syncScroll/set',
|
||||
MERGE_EDITOR_PREFERENCES = 'editor/preferences/merge',
|
||||
SET_LIGATURES = 'editor/preferences/setLigatures',
|
||||
SET_SMART_PASTE = 'editor/preferences/setSmartPaste'
|
||||
SET_SMART_PASTE = 'editor/preferences/setSmartPaste',
|
||||
SET_RENDERER_READY = 'editor/rendererReady/set'
|
||||
}
|
||||
|
||||
export interface EditorConfig {
|
||||
|
@ -21,6 +22,7 @@ export interface EditorConfig {
|
|||
syncScroll: boolean
|
||||
ligatures: boolean
|
||||
smartPaste: boolean
|
||||
rendererReady: boolean
|
||||
preferences: EditorConfiguration
|
||||
}
|
||||
|
||||
|
@ -28,6 +30,10 @@ export interface EditorConfigActions extends Action<EditorConfigActionType> {
|
|||
type: EditorConfigActionType
|
||||
}
|
||||
|
||||
export interface SetRendererReadyAction extends EditorConfigActions {
|
||||
rendererReady: boolean
|
||||
}
|
||||
|
||||
export interface SetEditorSyncScrollAction extends EditorConfigActions {
|
||||
syncScroll: boolean
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue