mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-21 02:35:23 -04:00
Wrap markdown rendering in iframe (#837)
Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
bd31076928
commit
586969f368
45 changed files with 1014 additions and 287 deletions
43
src/components/render-page/iframe-communicator.ts
Normal file
43
src/components/render-page/iframe-communicator.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export abstract class IframeCommunicator<SEND, RECEIVE> {
|
||||
protected otherSide?: Window
|
||||
protected otherOrigin?: string
|
||||
|
||||
constructor () {
|
||||
window.addEventListener("message", this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
public unregisterEventListener (): void {
|
||||
window.removeEventListener("message", this.handleEvent.bind(this))
|
||||
}
|
||||
|
||||
public setOtherSide (otherSide: Window, otherOrigin: string): void {
|
||||
this.otherSide = otherSide
|
||||
this.otherOrigin = otherOrigin
|
||||
}
|
||||
|
||||
public unsetOtherSide (): void {
|
||||
this.otherSide = undefined
|
||||
this.otherOrigin = undefined
|
||||
}
|
||||
|
||||
public getOtherSide (): Window | undefined {
|
||||
return this.otherSide
|
||||
}
|
||||
|
||||
protected sendMessageToOtherSide (message: SEND): void {
|
||||
if (this.otherSide === undefined || this.otherOrigin === undefined) {
|
||||
console.error("Can't send message because otherSide is null", message)
|
||||
return
|
||||
}
|
||||
this.otherSide.postMessage(message, this.otherOrigin)
|
||||
}
|
||||
|
||||
protected abstract handleEvent (event: MessageEvent<RECEIVE>): void;
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { ScrollState } from "../editor/scroll/scroll-props"
|
||||
import { YAMLMetaData } from "../editor/yaml-metadata/yaml-metadata"
|
||||
import { IframeCommunicator } from "./iframe-communicator"
|
||||
import {
|
||||
EditorToRendererIframeMessage,
|
||||
ImageDetails,
|
||||
RendererToEditorIframeMessage,
|
||||
RenderIframeMessageType
|
||||
} from "./rendering-message"
|
||||
|
||||
export class IframeEditorToRendererCommunicator extends IframeCommunicator<EditorToRendererIframeMessage, RendererToEditorIframeMessage> {
|
||||
private onSetScrollSourceToRendererHandler?: () => void
|
||||
private onTaskCheckboxChangeHandler?: (lineInMarkdown: number, checked: boolean) => void
|
||||
private onFirstHeadingChangeHandler?: (heading?: string) => void
|
||||
private onMetaDataChangeHandler?: (metaData?: YAMLMetaData) => void
|
||||
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
|
||||
private onRendererReadyHandler?: () => void
|
||||
private onImageClickedHandler?: (details: ImageDetails) => void
|
||||
|
||||
protected handleEvent (event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined {
|
||||
const renderMessage = event.data
|
||||
switch (renderMessage.type) {
|
||||
case RenderIframeMessageType.RENDERER_READY:
|
||||
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.ON_SET_META_DATA:
|
||||
this.onMetaDataChangeHandler?.(renderMessage.metaData)
|
||||
return false
|
||||
case RenderIframeMessageType.IMAGE_CLICKED:
|
||||
this.onImageClickedHandler?.(renderMessage.details)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
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 onMetaDataChange (handler?: (metaData?: YAMLMetaData) => void): void {
|
||||
this.onMetaDataChangeHandler = handler
|
||||
}
|
||||
|
||||
public onSetScrollState (handler?: (scrollState: ScrollState) => void): void {
|
||||
this.onSetScrollStateHandler = handler
|
||||
}
|
||||
|
||||
public sendSetBaseUrl (baseUrl: string): void {
|
||||
this.sendMessageToOtherSide({
|
||||
type: RenderIframeMessageType.SET_BASE_URL,
|
||||
baseUrl
|
||||
})
|
||||
}
|
||||
|
||||
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 sendSetWide (isWide: boolean): void {
|
||||
this.sendMessageToOtherSide({
|
||||
type: RenderIframeMessageType.SET_WIDE,
|
||||
activated: isWide
|
||||
})
|
||||
}
|
||||
|
||||
public sendScrollState (scrollState?: ScrollState): void {
|
||||
if (!scrollState) {
|
||||
return
|
||||
}
|
||||
this.sendMessageToOtherSide({
|
||||
type: RenderIframeMessageType.SET_SCROLL_STATE,
|
||||
scrollState
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { ScrollState } from "../editor/scroll/scroll-props"
|
||||
import { YAMLMetaData } from "../editor/yaml-metadata/yaml-metadata"
|
||||
import { IframeCommunicator } from "./iframe-communicator"
|
||||
import {
|
||||
EditorToRendererIframeMessage,
|
||||
ImageDetails,
|
||||
RendererToEditorIframeMessage,
|
||||
RenderIframeMessageType
|
||||
} from "./rendering-message"
|
||||
|
||||
export class IframeRendererToEditorCommunicator extends IframeCommunicator<RendererToEditorIframeMessage, EditorToRendererIframeMessage> {
|
||||
private onSetMarkdownContentHandler?: ((markdownContent: string) => void)
|
||||
private onSetDarkModeHandler?: ((darkModeActivated: boolean) => void)
|
||||
private onSetWideHandler?: ((wide: boolean) => void)
|
||||
private onSetScrollStateHandler?: ((scrollState: ScrollState) => void)
|
||||
private onSetBaseUrlHandler?: ((baseUrl: string) => void)
|
||||
|
||||
public onSetBaseUrl (handler?: (baseUrl: string) => void): void {
|
||||
this.onSetBaseUrlHandler = handler
|
||||
}
|
||||
|
||||
public onSetMarkdownContent (handler?: (markdownContent: string) => void): void {
|
||||
this.onSetMarkdownContentHandler = handler
|
||||
}
|
||||
|
||||
public onSetDarkMode (handler?: (darkModeActivated: boolean) => void): void {
|
||||
this.onSetDarkModeHandler = handler
|
||||
}
|
||||
|
||||
public onSetWide (handler?: (wide: boolean) => void): void {
|
||||
this.onSetWideHandler = handler
|
||||
}
|
||||
|
||||
public onSetScrollState (handler?: (scrollState: ScrollState) => void): void {
|
||||
this.onSetScrollStateHandler = handler
|
||||
}
|
||||
|
||||
public sendRendererReady (): void {
|
||||
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 sendSetMetaData (metaData: YAMLMetaData | undefined): void {
|
||||
this.sendMessageToOtherSide({
|
||||
type: RenderIframeMessageType.ON_SET_META_DATA,
|
||||
metaData
|
||||
})
|
||||
}
|
||||
|
||||
public sendSetScrollState (scrollState: ScrollState): void {
|
||||
this.sendMessageToOtherSide({
|
||||
type: RenderIframeMessageType.SET_SCROLL_STATE,
|
||||
scrollState
|
||||
})
|
||||
}
|
||||
|
||||
protected handleEvent (event: MessageEvent<EditorToRendererIframeMessage>): boolean | undefined {
|
||||
const renderMessage = event.data
|
||||
switch (renderMessage.type) {
|
||||
case RenderIframeMessageType.SET_MARKDOWN_CONTENT:
|
||||
this.onSetMarkdownContentHandler?.(renderMessage.content)
|
||||
return false
|
||||
case RenderIframeMessageType.SET_DARKMODE:
|
||||
this.onSetDarkModeHandler?.(renderMessage.activated)
|
||||
return false
|
||||
case RenderIframeMessageType.SET_WIDE:
|
||||
this.onSetWideHandler?.(renderMessage.activated)
|
||||
return false
|
||||
case RenderIframeMessageType.SET_SCROLL_STATE:
|
||||
this.onSetScrollStateHandler?.(renderMessage.scrollState)
|
||||
return false
|
||||
case RenderIframeMessageType.SET_BASE_URL:
|
||||
this.onSetBaseUrlHandler?.(renderMessage.baseUrl)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public sendClickedImageUrl (details: ImageDetails): void {
|
||||
this.sendMessageToOtherSide({
|
||||
type: RenderIframeMessageType.IMAGE_CLICKED,
|
||||
details: details
|
||||
})
|
||||
}
|
||||
}
|
105
src/components/render-page/render-page.tsx
Normal file
105
src/components/render-page/render-page.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import equal from "fast-deep-equal"
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
||||
import { ApplicationState } from '../../redux'
|
||||
import { setDarkMode } from '../../redux/dark-mode/methods'
|
||||
import { setDocumentMetadata } from '../../redux/document-content/methods'
|
||||
import { ScrollingDocumentRenderPane } from '../editor/document-renderer-pane/scrolling-document-render-pane'
|
||||
import { ScrollState } from '../editor/scroll/scroll-props'
|
||||
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
|
||||
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
||||
import { IframeRendererToEditorCommunicator } from './iframe-renderer-to-editor-communicator'
|
||||
|
||||
export const RenderPage: React.FC = () => {
|
||||
useApplyDarkMode()
|
||||
|
||||
const [markdownContent, setMarkdownContent] = useState('')
|
||||
const [isWide, setWide] = useState(false)
|
||||
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
|
||||
const [baseUrl, setBaseUrl] = useState<string>()
|
||||
|
||||
const editorOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.editorOrigin)
|
||||
|
||||
const iframeCommunicator = useMemo(() => {
|
||||
const newCommunicator = new IframeRendererToEditorCommunicator()
|
||||
newCommunicator.setOtherSide(window.parent, editorOrigin)
|
||||
return newCommunicator
|
||||
}, [editorOrigin])
|
||||
|
||||
useEffect(() => {
|
||||
iframeCommunicator.sendRendererReady()
|
||||
return () => iframeCommunicator.unregisterEventListener()
|
||||
}, [iframeCommunicator])
|
||||
|
||||
useEffect(() => iframeCommunicator.onSetBaseUrl(setBaseUrl), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator.onSetWide(setWide), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator.onSetScrollState((newScrollState) => {
|
||||
if (!equal(scrollState, newScrollState)) {
|
||||
setScrollState(newScrollState)
|
||||
}
|
||||
}), [iframeCommunicator, scrollState])
|
||||
|
||||
const onTaskCheckedChange = useCallback((lineInMarkdown: number, checked: boolean) => {
|
||||
iframeCommunicator.sendTaskCheckBoxChange(lineInMarkdown, checked)
|
||||
}, [iframeCommunicator])
|
||||
|
||||
const onFirstHeadingChange = useCallback((firstHeading?: string) => {
|
||||
iframeCommunicator.sendFirstHeadingChanged(firstHeading)
|
||||
}, [iframeCommunicator])
|
||||
|
||||
const onMakeScrollSource = useCallback(() => {
|
||||
iframeCommunicator.sendSetScrollSourceToRenderer()
|
||||
}, [iframeCommunicator])
|
||||
|
||||
const onMetaDataChange = useCallback((metaData?: YAMLMetaData) => {
|
||||
setDocumentMetadata(metaData)
|
||||
iframeCommunicator.sendSetMetaData(metaData)
|
||||
}, [iframeCommunicator])
|
||||
|
||||
const onScroll = useCallback((scrollState: ScrollState) => {
|
||||
iframeCommunicator.sendSetScrollState(scrollState)
|
||||
}, [iframeCommunicator])
|
||||
|
||||
const onImageClick: ImageClickHandler = useCallback((event) => {
|
||||
const image = event.target as HTMLImageElement
|
||||
if (image.src === '') {
|
||||
return
|
||||
}
|
||||
iframeCommunicator.sendClickedImageUrl({
|
||||
src: image.src,
|
||||
alt: image.alt,
|
||||
title: image.title
|
||||
})
|
||||
}, [iframeCommunicator])
|
||||
|
||||
if (!baseUrl) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"vh-100 w-100"}>
|
||||
<ScrollingDocumentRenderPane
|
||||
extraClasses={'w-100'}
|
||||
markdownContent={markdownContent}
|
||||
wide={isWide}
|
||||
onTaskCheckedChange={onTaskCheckedChange}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onMakeScrollSource={onMakeScrollSource}
|
||||
onMetadataChange={onMetaDataChange}
|
||||
scrollState={scrollState}
|
||||
onScroll={onScroll}
|
||||
baseUrl={baseUrl}
|
||||
onImageClick={onImageClick}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RenderPage
|
92
src/components/render-page/rendering-message.ts
Normal file
92
src/components/render-page/rendering-message.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { ScrollState } from '../editor/scroll/scroll-props'
|
||||
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
|
||||
|
||||
export enum RenderIframeMessageType {
|
||||
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
||||
RENDERER_READY = 'RENDERER_READY',
|
||||
SET_DARKMODE = 'SET_DARKMODE',
|
||||
SET_WIDE = 'SET_WIDE',
|
||||
ON_TASK_CHECKBOX_CHANGE = 'ON_TASK_CHECKBOX_CHANGE',
|
||||
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
|
||||
SET_SCROLL_SOURCE_TO_RENDERER = 'SET_SCROLL_SOURCE_TO_RENDERER',
|
||||
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
||||
ON_SET_META_DATA = 'ON_SET_META_DATA',
|
||||
IMAGE_CLICKED = 'IMAGE_CLICKED',
|
||||
SET_BASE_URL = 'SET_BASE_URL'
|
||||
}
|
||||
|
||||
export interface RendererToEditorSimpleMessage {
|
||||
type: RenderIframeMessageType.RENDERER_READY | RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER
|
||||
}
|
||||
|
||||
export interface SetDarkModeMessage {
|
||||
type: RenderIframeMessageType.SET_DARKMODE,
|
||||
activated: boolean
|
||||
}
|
||||
|
||||
export interface ImageDetails {
|
||||
alt?: string
|
||||
src: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface SetBaseUrlMessage {
|
||||
type: RenderIframeMessageType.SET_BASE_URL,
|
||||
baseUrl: string
|
||||
}
|
||||
|
||||
export interface ImageClickedMessage {
|
||||
type: RenderIframeMessageType.IMAGE_CLICKED,
|
||||
details: ImageDetails
|
||||
}
|
||||
|
||||
export interface SetWideMessage {
|
||||
type: RenderIframeMessageType.SET_WIDE,
|
||||
activated: boolean
|
||||
}
|
||||
|
||||
export interface SetMarkdownContentMessage {
|
||||
type: RenderIframeMessageType.SET_MARKDOWN_CONTENT,
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface SetScrollStateMessage {
|
||||
type: RenderIframeMessageType.SET_SCROLL_STATE,
|
||||
scrollState: ScrollState
|
||||
}
|
||||
|
||||
export interface OnTaskCheckboxChangeMessage {
|
||||
type: RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE,
|
||||
lineInMarkdown: number,
|
||||
checked: boolean
|
||||
}
|
||||
|
||||
export interface OnFirstHeadingChangeMessage {
|
||||
type: RenderIframeMessageType.ON_FIRST_HEADING_CHANGE,
|
||||
firstHeading: string | undefined
|
||||
}
|
||||
|
||||
export interface OnMetadataChangeMessage {
|
||||
type: RenderIframeMessageType.ON_SET_META_DATA,
|
||||
metaData: YAMLMetaData | undefined
|
||||
}
|
||||
|
||||
export type EditorToRendererIframeMessage =
|
||||
SetMarkdownContentMessage |
|
||||
SetDarkModeMessage |
|
||||
SetWideMessage |
|
||||
SetScrollStateMessage |
|
||||
SetBaseUrlMessage
|
||||
|
||||
export type RendererToEditorIframeMessage =
|
||||
RendererToEditorSimpleMessage |
|
||||
OnFirstHeadingChangeMessage |
|
||||
OnTaskCheckboxChangeMessage |
|
||||
OnMetadataChangeMessage |
|
||||
SetScrollStateMessage |
|
||||
ImageClickedMessage
|
Loading…
Add table
Add a link
Reference in a new issue