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:
Tilman Vatteroth 2021-02-08 15:03:11 +01:00 committed by GitHub
parent 4b2e2a7c93
commit 7f6e0e53a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 373 additions and 173 deletions

View 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])
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -8,8 +8,7 @@
width: 100%;
height: 100%;
margin: 0;
overflow-y: scroll;
overflow-x: auto;
overflow: auto;
display: flex;
flex-direction: row;

View file

@ -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' }>

View file

@ -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

View file

@ -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
}