mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-23 19:47:03 -04:00
Add custom intro page by fetching markdown content from a file (#697)
Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
4b2e2a7c93
commit
7f6e0e53a7
31 changed files with 373 additions and 173 deletions
23
src/components/render-page/hooks/use-image-click-handler.ts
Normal file
23
src/components/render-page/hooks/use-image-click-handler.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react'
|
||||
import { ImageClickHandler } from '../../markdown-renderer/replace-components/image/image-replacer'
|
||||
import { IframeRendererToEditorCommunicator } from '../iframe-renderer-to-editor-communicator'
|
||||
|
||||
export const useImageClickHandler = (iframeCommunicator: IframeRendererToEditorCommunicator): ImageClickHandler => {
|
||||
return useCallback((event: React.MouseEvent<HTMLImageElement, MouseEvent>) => {
|
||||
const image = event.target as HTMLImageElement
|
||||
if (image.src === '') {
|
||||
return
|
||||
}
|
||||
iframeCommunicator.sendClickedImageUrl({
|
||||
src: image.src,
|
||||
alt: image.alt,
|
||||
title: image.title
|
||||
})
|
||||
}, [iframeCommunicator])
|
||||
}
|
|
@ -8,6 +8,7 @@ import { NoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatte
|
|||
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||
import { IframeCommunicator } from './iframe-communicator'
|
||||
import {
|
||||
BaseConfiguration,
|
||||
EditorToRendererIframeMessage,
|
||||
ImageDetails,
|
||||
RendererToEditorIframeMessage,
|
||||
|
@ -22,6 +23,11 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<Edito
|
|||
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
|
||||
private onRendererReadyHandler?: () => void
|
||||
private onImageClickedHandler?: (details: ImageDetails) => void
|
||||
private onHeightChangeHandler?: (height: number) => void
|
||||
|
||||
public onHeightChange(handler?: (height: number) => void): void {
|
||||
this.onHeightChangeHandler = handler
|
||||
}
|
||||
|
||||
public onFrontmatterChange(handler?: (frontmatter?: NoteFrontmatter) => void): void {
|
||||
this.onFrontmatterChangeHandler = handler
|
||||
|
@ -51,10 +57,10 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<Edito
|
|||
this.onSetScrollStateHandler = handler
|
||||
}
|
||||
|
||||
public sendSetBaseUrl(baseUrl: string): void {
|
||||
public sendSetBaseConfiguration(baseConfiguration: BaseConfiguration): void {
|
||||
this.sendMessageToOtherSide({
|
||||
type: RenderIframeMessageType.SET_BASE_URL,
|
||||
baseUrl
|
||||
type: RenderIframeMessageType.SET_BASE_CONFIGURATION,
|
||||
baseConfiguration
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -106,6 +112,9 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<Edito
|
|||
case RenderIframeMessageType.IMAGE_CLICKED:
|
||||
this.onImageClickedHandler?.(renderMessage.details)
|
||||
return false
|
||||
case RenderIframeMessageType.ON_HEIGHT_CHANGE:
|
||||
this.onHeightChangeHandler?.(renderMessage.height)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { NoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatte
|
|||
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||
import { IframeCommunicator } from './iframe-communicator'
|
||||
import {
|
||||
BaseConfiguration,
|
||||
EditorToRendererIframeMessage,
|
||||
ImageDetails,
|
||||
RendererToEditorIframeMessage,
|
||||
|
@ -18,10 +19,10 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<Rende
|
|||
private onSetMarkdownContentHandler?: ((markdownContent: string) => void)
|
||||
private onSetDarkModeHandler?: ((darkModeActivated: boolean) => void)
|
||||
private onSetScrollStateHandler?: ((scrollState: ScrollState) => void)
|
||||
private onSetBaseUrlHandler?: ((baseUrl: string) => void)
|
||||
private onSetBaseConfigurationHandler?: ((baseConfiguration: BaseConfiguration) => void)
|
||||
|
||||
public onSetBaseUrl(handler?: (baseUrl: string) => void): void {
|
||||
this.onSetBaseUrlHandler = handler
|
||||
public onSetBaseConfiguration(handler?: (baseConfiguration: BaseConfiguration) => void): void {
|
||||
this.onSetBaseConfigurationHandler = handler
|
||||
}
|
||||
|
||||
public onSetMarkdownContent(handler?: (markdownContent: string) => void): void {
|
||||
|
@ -84,6 +85,13 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<Rende
|
|||
})
|
||||
}
|
||||
|
||||
public sendHeightChange(height: number): void {
|
||||
this.sendMessageToOtherSide({
|
||||
type: RenderIframeMessageType.ON_HEIGHT_CHANGE,
|
||||
height
|
||||
})
|
||||
}
|
||||
|
||||
protected handleEvent(event: MessageEvent<EditorToRendererIframeMessage>): boolean | undefined {
|
||||
const renderMessage = event.data
|
||||
switch (renderMessage.type) {
|
||||
|
@ -96,8 +104,8 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<Rende
|
|||
case RenderIframeMessageType.SET_SCROLL_STATE:
|
||||
this.onSetScrollStateHandler?.(renderMessage.scrollState)
|
||||
return false
|
||||
case RenderIframeMessageType.SET_BASE_URL:
|
||||
this.onSetBaseUrlHandler?.(renderMessage.baseUrl)
|
||||
case RenderIframeMessageType.SET_BASE_CONFIGURATION:
|
||||
this.onSetBaseConfigurationHandler?.(renderMessage.baseConfiguration)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow-y: scroll;
|
||||
overflow-x: auto;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import React, { MutableRefObject, useMemo, useRef, useState } from 'react'
|
||||
import React, { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
|
||||
|
@ -19,8 +19,7 @@ import { FullMarkdownRenderer } from '../markdown-renderer/full-markdown-rendere
|
|||
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
||||
import './markdown-document.scss'
|
||||
|
||||
export interface MarkdownDocumentProps extends ScrollProps {
|
||||
extraClasses?: string
|
||||
export interface RendererProps extends ScrollProps {
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
|
@ -28,11 +27,19 @@ export interface MarkdownDocumentProps extends ScrollProps {
|
|||
markdownContent: string,
|
||||
baseUrl?: string
|
||||
onImageClick?: ImageClickHandler
|
||||
onHeightChange?: (height: number) => void
|
||||
disableToc?: boolean
|
||||
}
|
||||
|
||||
export interface MarkdownDocumentProps extends RendererProps {
|
||||
additionalOuterContainerClasses?: string
|
||||
additionalRendererClasses?: string
|
||||
}
|
||||
|
||||
export const MarkdownDocument: React.FC<MarkdownDocumentProps> = (
|
||||
{
|
||||
extraClasses,
|
||||
additionalOuterContainerClasses,
|
||||
additionalRendererClasses,
|
||||
onFirstHeadingChange,
|
||||
onFrontmatterChange,
|
||||
onMakeScrollSource,
|
||||
|
@ -41,42 +48,53 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = (
|
|||
markdownContent,
|
||||
onImageClick,
|
||||
onScroll,
|
||||
scrollState
|
||||
scrollState,
|
||||
onHeightChange,
|
||||
disableToc
|
||||
}) => {
|
||||
const rendererRef = useRef<HTMLDivElement | null>(null)
|
||||
const internalDocumentRenderPaneRef = useRef<HTMLDivElement>(null)
|
||||
const [tocAst, setTocAst] = useState<TocAst>()
|
||||
const width = useResizeObserver({ ref: internalDocumentRenderPaneRef.current }).width ?? 0
|
||||
|
||||
const internalDocumentRenderPaneSize = useResizeObserver({ ref: internalDocumentRenderPaneRef.current })
|
||||
const rendererSize = useResizeObserver({ ref: rendererRef.current })
|
||||
|
||||
const containerWidth = internalDocumentRenderPaneSize.width ?? 0
|
||||
|
||||
useEffect(() => {
|
||||
if (!onHeightChange) {
|
||||
return
|
||||
}
|
||||
onHeightChange(rendererSize.height ? rendererSize.height + 1 : 0)
|
||||
}, [rendererSize.height, onHeightChange])
|
||||
|
||||
const contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent])
|
||||
const [onLineMarkerPositionChanged, onUserScroll] = useSyncedScrolling(internalDocumentRenderPaneRef, rendererRef, contentLineCount, scrollState, onScroll)
|
||||
|
||||
return (
|
||||
<div className={ `markdown-document ${ extraClasses ?? '' }` }
|
||||
<div className={ `markdown-document ${ additionalOuterContainerClasses ?? '' }` }
|
||||
ref={ internalDocumentRenderPaneRef } onScroll={ onUserScroll } onMouseEnter={ onMakeScrollSource }>
|
||||
<div className={ 'markdown-document-side' }/>
|
||||
<div className={ 'bg-light markdown-document-content' }>
|
||||
<div className={ 'markdown-document-content' }>
|
||||
<YamlArrayDeprecationAlert/>
|
||||
<FullMarkdownRenderer
|
||||
rendererRef={ rendererRef }
|
||||
className={ 'flex-fill pt-4 mb-3' }
|
||||
className={ `flex-fill mb-3 ${ additionalRendererClasses ?? '' }` }
|
||||
content={ markdownContent }
|
||||
onFirstHeadingChange={ onFirstHeadingChange }
|
||||
onLineMarkerPositionChanged={ onLineMarkerPositionChanged }
|
||||
onFrontmatterChange={ onFrontmatterChange }
|
||||
onTaskCheckedChange={ onTaskCheckedChange }
|
||||
onTocChange={ (tocAst) => setTocAst(tocAst) }
|
||||
onTocChange={ setTocAst }
|
||||
baseUrl={ baseUrl }
|
||||
onImageClick={ onImageClick }/>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={ 'markdown-document-side pt-4' }>
|
||||
<ShowIf condition={ !!tocAst }>
|
||||
<ShowIf condition={ width >= 1100 }>
|
||||
<ShowIf condition={ !!tocAst && !disableToc }>
|
||||
<ShowIf condition={ containerWidth >= 1100 }>
|
||||
<TableOfContents ast={ tocAst as TocAst } className={ 'sticky' } baseUrl={ baseUrl }/>
|
||||
</ShowIf>
|
||||
<ShowIf condition={ width < 1100 }>
|
||||
<ShowIf condition={ containerWidth < 1100 }>
|
||||
<div className={ 'markdown-toc-sidebar-button' }>
|
||||
<Dropdown drop={ 'up' }>
|
||||
<Dropdown.Toggle id="toc-overlay-button" variant={ 'secondary' } className={ 'no-arrow' }>
|
||||
|
|
|
@ -12,15 +12,17 @@ import { setNoteFrontmatter } from '../../redux/note-details/methods'
|
|||
import { NoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
||||
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
||||
import { useImageClickHandler } from './hooks/use-image-click-handler'
|
||||
import { IframeRendererToEditorCommunicator } from './iframe-renderer-to-editor-communicator'
|
||||
import { MarkdownDocument } from './markdown-document'
|
||||
import { BaseConfiguration, RendererType } from './rendering-message'
|
||||
|
||||
export const RenderPage: React.FC = () => {
|
||||
useApplyDarkMode()
|
||||
|
||||
const [markdownContent, setMarkdownContent] = useState('')
|
||||
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
|
||||
const [baseUrl, setBaseUrl] = useState<string>()
|
||||
const [baseConfiguration, setBaseConfiguration] = useState<BaseConfiguration | undefined>(undefined)
|
||||
|
||||
const editorOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.editorOrigin)
|
||||
|
||||
|
@ -35,7 +37,7 @@ export const RenderPage: React.FC = () => {
|
|||
return () => iframeCommunicator.unregisterEventListener()
|
||||
}, [iframeCommunicator])
|
||||
|
||||
useEffect(() => iframeCommunicator.onSetBaseUrl(setBaseUrl), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator.onSetBaseConfiguration(setBaseConfiguration), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
|
||||
|
@ -61,37 +63,45 @@ export const RenderPage: React.FC = () => {
|
|||
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
|
||||
})
|
||||
const onImageClick: ImageClickHandler = useImageClickHandler(iframeCommunicator)
|
||||
|
||||
const onHeightChange = useCallback((height: number) => {
|
||||
iframeCommunicator.sendHeightChange(height)
|
||||
}, [iframeCommunicator])
|
||||
|
||||
if (!baseUrl) {
|
||||
if (!baseConfiguration) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ 'vh-100 w-100' }>
|
||||
<MarkdownDocument
|
||||
extraClasses={ 'bg-light' }
|
||||
markdownContent={ markdownContent }
|
||||
onTaskCheckedChange={ onTaskCheckedChange }
|
||||
onFirstHeadingChange={ onFirstHeadingChange }
|
||||
onMakeScrollSource={ onMakeScrollSource }
|
||||
onFrontmatterChange={ onFrontmatterChange }
|
||||
scrollState={ scrollState }
|
||||
onScroll={ onScroll }
|
||||
baseUrl={ baseUrl }
|
||||
onImageClick={ onImageClick }/>
|
||||
</div>
|
||||
)
|
||||
switch (baseConfiguration.rendererType) {
|
||||
case RendererType.DOCUMENT:
|
||||
return (
|
||||
<MarkdownDocument
|
||||
additionalOuterContainerClasses={ 'vh-100 bg-light' }
|
||||
additionalRendererClasses={ 'mb-3' }
|
||||
markdownContent={ markdownContent }
|
||||
onTaskCheckedChange={ onTaskCheckedChange }
|
||||
onFirstHeadingChange={ onFirstHeadingChange }
|
||||
onMakeScrollSource={ onMakeScrollSource }
|
||||
onFrontmatterChange={ onFrontmatterChange }
|
||||
scrollState={ scrollState }
|
||||
onScroll={ onScroll }
|
||||
baseUrl={ baseConfiguration.baseUrl }
|
||||
onImageClick={ onImageClick }/>
|
||||
)
|
||||
case RendererType.INTRO:
|
||||
return (
|
||||
<MarkdownDocument
|
||||
additionalOuterContainerClasses={ 'vh-100 bg-light overflow-y-hidden' }
|
||||
markdownContent={ markdownContent }
|
||||
baseUrl={ baseConfiguration.baseUrl }
|
||||
onImageClick={ onImageClick }
|
||||
disableToc={ true }
|
||||
onHeightChange={ onHeightChange }/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default RenderPage
|
||||
|
|
|
@ -16,7 +16,8 @@ export enum RenderIframeMessageType {
|
|||
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
||||
ON_SET_FRONTMATTER = 'ON_SET_FRONTMATTER',
|
||||
IMAGE_CLICKED = 'IMAGE_CLICKED',
|
||||
SET_BASE_URL = 'SET_BASE_URL'
|
||||
ON_HEIGHT_CHANGE = 'ON_HEIGHT_CHANGE',
|
||||
SET_BASE_CONFIGURATION = 'SET_BASE_CONFIGURATION'
|
||||
}
|
||||
|
||||
export interface RendererToEditorSimpleMessage {
|
||||
|
@ -35,8 +36,8 @@ export interface ImageDetails {
|
|||
}
|
||||
|
||||
export interface SetBaseUrlMessage {
|
||||
type: RenderIframeMessageType.SET_BASE_URL,
|
||||
baseUrl: string
|
||||
type: RenderIframeMessageType.SET_BASE_CONFIGURATION,
|
||||
baseConfiguration: BaseConfiguration
|
||||
}
|
||||
|
||||
export interface ImageClickedMessage {
|
||||
|
@ -70,6 +71,11 @@ export interface OnFrontmatterChangeMessage {
|
|||
frontmatter: NoteFrontmatter | undefined
|
||||
}
|
||||
|
||||
export interface OnHeightChangeMessage {
|
||||
type: RenderIframeMessageType.ON_HEIGHT_CHANGE,
|
||||
height: number
|
||||
}
|
||||
|
||||
export type EditorToRendererIframeMessage =
|
||||
SetMarkdownContentMessage |
|
||||
SetDarkModeMessage |
|
||||
|
@ -82,4 +88,15 @@ export type RendererToEditorIframeMessage =
|
|||
OnTaskCheckboxChangeMessage |
|
||||
OnFrontmatterChangeMessage |
|
||||
SetScrollStateMessage |
|
||||
ImageClickedMessage
|
||||
ImageClickedMessage |
|
||||
OnHeightChangeMessage
|
||||
|
||||
export enum RendererType {
|
||||
DOCUMENT,
|
||||
INTRO
|
||||
}
|
||||
|
||||
export interface BaseConfiguration {
|
||||
baseUrl: string
|
||||
rendererType: RendererType
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue