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

@ -27,6 +27,10 @@ Files: public/index.html
Copyright: 2021 The HedgeDoc developers (see AUTHORS file)
License: CC0-1.0
Files: public/intro.md
Copyright: 2021 The HedgeDoc developers (see AUTHORS file)
License: CC0-1.0
Files: public/robots.txt
Copyright: 2021 The HedgeDoc developers (see AUTHORS file)
License: CC0-1.0

View file

@ -69,6 +69,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
- Surround selected text with a link via shortcut (ctrl+k or cmd+k).
- A sidebar for menu options
- Improved security by wrapping the markdown rendering into an iframe
- The intro page content can be changed by editing `public/intro.md`
### Changed

View file

@ -5,50 +5,58 @@
*/
/* eslint-disable @typescript-eslint/no-unsafe-call */
describe('Intro', () => {
describe('Intro page', () => {
beforeEach(() => {
cy.intercept('/intro.md', 'test content')
cy.visit('/')
})
describe('Cover Button are hidden when logged in', () => {
it('Sign in Cover Button', () => {
cy.get('.cover-button.btn-success')
.should('not.exist')
})
it('Features Cover Button', () => {
cy.get('.cover-button.btn-primary')
.should('not.exist')
describe('content', () => {
it('fetches and shows the correct intro page content', () => {
cy.getMarkdownBody()
.contains('test content')
})
})
describe('Cover Button are shown when logged out', () => {
beforeEach(() => {
describe('features button', () => {
it('is hidden when logged in', () => {
cy.get('[data-cy="features-button"]')
.should('not.exist')
})
it('is visible when logged out', () => {
cy.logout()
})
it('Sign in Cover Button', () => {
cy.get('.cover-button.btn-success')
.should('exist')
})
it('Features Cover Button', () => {
cy.get('.cover-button.btn-primary')
cy.get('[data-cy="features-button"]')
.should('exist')
})
})
it('Version can be opened and closed', () => {
cy.get('#versionModal')
.should('not.exist')
cy.get('#version')
.click()
cy.get('#versionModal')
.should('be.visible')
cy.get('#versionModal .modal-footer .btn')
.contains('Close')
.click()
cy.get('#versionModal')
.should('not.exist')
describe('sign in button', () => {
it('is hidden when logged in', () => {
cy.get('[data-cy="sign-in-button"]')
.should('not.exist')
})
it('is visible when logged out', () => {
cy.logout()
cy.get('[data-cy="sign-in-button"]')
.should('exist')
})
})
describe('version dialog', () => {
it('can be opened and closed', () => {
cy.get('[data-cy="version-modal"]')
.should('not.exist')
cy.get('[data-cy="show-version-modal"]')
.click()
cy.get('[data-cy="version-modal"]')
.should('be.visible')
cy.get('[data-cy="version-modal"] [data-cy="close-version-modal-button"]')
.contains('Close')
.click()
cy.get('[data-cy="version-modal"]')
.should('not.exist')
})
})
})

View file

@ -94,21 +94,21 @@ describe('Links Intro', () => {
describe('Feature Links', () => {
it('Share-Notes', () => {
cy.get('i.fa-bolt.fa-3x')
cy.get('i.fa-bolt')
.click()
cy.url()
.should('include', '/features#Share-Notes')
})
it('KaTeX', () => {
cy.get('i.fa-bar-chart.fa-3x')
cy.get('i.fa-bar-chart')
.click()
cy.url()
.should('include', '/features#MathJax')
})
it('Slide-Mode', () => {
cy.get('i.fa-television.fa-3x')
cy.get('i.fa-television')
.click()
cy.url()
.should('include', '/features#Slide-Mode')

View file

@ -14,6 +14,7 @@ declare namespace Cypress {
Cypress.Commands.add('getMarkdownRenderer', () => {
return cy.get(`iframe[data-cy="documentIframe"]`)
.should('be.visible')
.its('0.contentDocument')
.should('exist')
.its('body')

5
public/intro.md Normal file
View file

@ -0,0 +1,5 @@
:::warning
What you see is an UI-Test! It's filled with dummy data, not connected to a backend and no data will be saved.
:::
![HedgeDoc Screenshot](screenshot.png)

View file

@ -34,7 +34,8 @@
"katex": "Works with charts and KaTeX",
"slides": "Supports slide mode"
},
"screenShotAltText": "HedgeDoc Screenshot"
"markdownWhileLoading": "Loading...",
"markdownLoadingError": "Error while fetching intro content"
},
"history": {
"error": {

BIN
public/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

View file

@ -22,6 +22,7 @@ import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
import { DocumentInfobar } from './document-infobar'
import { ErrorWhileLoadingNoteAlert } from './ErrorWhileLoadingNoteAlert'
import { LoadingNoteAlert } from './LoadingNoteAlert'
import { RendererType } from '../render-page/rendering-message'
export const DocumentReadOnlyPage: React.FC = () => {
@ -55,10 +56,11 @@ export const DocumentReadOnlyPage: React.FC = () => {
noteId={ id }
viewCount={ noteDetails.viewCount }
/>
<RenderIframe extraClasses={ 'flex-fill' }
<RenderIframe frameClasses={ 'flex-fill h-100 w-100' }
markdownContent={ markdownContent }
onFirstHeadingChange={ onFirstHeadingChange }
onFrontmatterChange={ onFrontmatterChange }/>
onFrontmatterChange={ onFrontmatterChange }
rendererType={RendererType.DOCUMENT}/>
</ShowIf>
</div>
)

View file

@ -32,6 +32,7 @@ import { RenderIframe } from './renderer-pane/render-iframe'
import { Sidebar } from './sidebar/sidebar'
import { Splitter } from './splitter/splitter'
import { DualScrollState, ScrollState } from './synced-scroll/scroll-props'
import { RendererType } from '../render-page/rendering-message'
export interface EditorPagePathParams {
id: string
@ -115,13 +116,15 @@ export const EditorPage: React.FC = () => {
showRight={ editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH }
right={
<RenderIframe
frameClasses={'h-100 w-100'}
markdownContent={ markdownContent }
onMakeScrollSource={ setRendererToScrollSource }
onFirstHeadingChange={ updateNoteTitleByFirstHeading }
onTaskCheckedChange={ SetCheckboxInMarkdownContent }
onFrontmatterChange={ setNoteFrontmatter }
onScroll={ onMarkdownRendererScroll }
scrollState={ scrollState.rendererScrollState }/>
scrollState={ scrollState.rendererScrollState }
rendererType={ RendererType.DOCUMENT }/>
}
containerClassName={ 'overflow-hidden' }/>
<Sidebar/>

View file

@ -10,13 +10,20 @@ import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-a
import { ApplicationState } from '../../../redux'
import { isTestMode } from '../../../utils/is-test-mode'
import { IframeEditorToRendererCommunicator } from '../../render-page/iframe-editor-to-renderer-communicator'
import { MarkdownDocumentProps } from '../../render-page/markdown-document'
import { ImageDetails } from '../../render-page/rendering-message'
import { RendererProps } from '../../render-page/markdown-document'
import { ImageDetails, RendererType } from '../../render-page/rendering-message'
import { ScrollState } from '../synced-scroll/scroll-props'
import { useOnIframeLoad } from './hooks/use-on-iframe-load'
import { ShowOnPropChangeImageLightbox } from './show-on-prop-change-image-lightbox'
export const RenderIframe: React.FC<MarkdownDocumentProps> = (
export interface RenderIframeProps extends RendererProps {
onRendererReadyChange?: (rendererReady: boolean) => void
rendererType: RendererType,
forcedDarkMode?: boolean
frameClasses?: string
}
export const RenderIframe: React.FC<RenderIframeProps> = (
{
markdownContent,
onTaskCheckedChange,
@ -25,9 +32,13 @@ export const RenderIframe: React.FC<MarkdownDocumentProps> = (
onFirstHeadingChange,
onScroll,
onMakeScrollSource,
extraClasses
frameClasses,
onRendererReadyChange,
rendererType,
forcedDarkMode
}) => {
const darkMode = useIsDarkModeActivated()
const savedDarkMode = useIsDarkModeActivated()
const darkMode = forcedDarkMode ?? savedDarkMode
const [rendererReady, setRendererReady] = useState<boolean>(false)
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
@ -37,6 +48,11 @@ export const RenderIframe: React.FC<MarkdownDocumentProps> = (
const resetRendererReady = useCallback(() => setRendererReady(false), [])
const iframeCommunicator = useMemo(() => new IframeEditorToRendererCommunicator(), [])
const onIframeLoad = useOnIframeLoad(frameReference, iframeCommunicator, rendererOrigin, renderPageUrl, resetRendererReady)
const [frameHeight, setFrameHeight] = useState<number>(0)
useEffect(() => {
onRendererReadyChange?.(rendererReady)
}, [onRendererReadyChange, rendererReady])
useEffect(() => () => iframeCommunicator.unregisterEventListener(), [iframeCommunicator])
useEffect(() => iframeCommunicator.onFirstHeadingChange(onFirstHeadingChange), [iframeCommunicator,
@ -49,8 +65,15 @@ export const RenderIframe: React.FC<MarkdownDocumentProps> = (
useEffect(() => iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange), [iframeCommunicator,
onTaskCheckedChange])
useEffect(() => iframeCommunicator.onImageClicked(setLightboxDetails), [iframeCommunicator])
useEffect(() => iframeCommunicator.onRendererReady(() => setRendererReady(true)), [darkMode, iframeCommunicator,
scrollState])
useEffect(() => iframeCommunicator.onRendererReady(() => {
iframeCommunicator.sendSetBaseConfiguration({
baseUrl: window.location.toString(),
rendererType
})
setRendererReady(true)
}), [darkMode, rendererType, iframeCommunicator, rendererReady, scrollState])
useEffect(() => iframeCommunicator.onHeightChange(setFrameHeight), [iframeCommunicator])
useEffect(() => {
if (rendererReady) {
iframeCommunicator.sendSetDarkmode(darkMode)
@ -65,12 +88,6 @@ export const RenderIframe: React.FC<MarkdownDocumentProps> = (
}
}, [iframeCommunicator, rendererReady, scrollState])
useEffect(() => {
if (rendererReady) {
iframeCommunicator.sendSetBaseUrl(window.location.toString())
}
}, [iframeCommunicator, rendererReady])
useEffect(() => {
if (rendererReady) {
iframeCommunicator.sendSetMarkdownContent(markdownContent)
@ -79,8 +96,9 @@ export const RenderIframe: React.FC<MarkdownDocumentProps> = (
return <Fragment>
<ShowOnPropChangeImageLightbox details={ lightboxDetails }/>
<iframe data-cy={ 'documentIframe' } onLoad={ onIframeLoad } title="render" src={ renderPageUrl }
<iframe style={ { height: `${ frameHeight }px` } } data-cy={ 'documentIframe' } onLoad={ onIframeLoad }
title="render" src={ renderPageUrl }
{ ...isTestMode() ? {} : { sandbox: 'allow-downloads allow-same-origin allow-scripts allow-popups' } }
ref={ frameReference } className={ `h-100 w-100 border-0 ${ extraClasses ?? '' }` }/>
ref={ frameReference } className={ `border-0 ${ frameClasses ?? '' }` }/>
</Fragment>
}

View file

@ -1,7 +1,7 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
@ -38,6 +38,7 @@ export const CoverButtons: React.FC = () => {
</ShowIf>
<Link to="/n/features">
<Button
data-cy={ 'features-button' }
className="cover-button"
variant="primary"
size="lg"

View file

@ -13,29 +13,29 @@ import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
export const FeatureLinks: React.FC = () => {
useTranslation()
return (
<Row className="mb-5">
<Col md={ 4 }>
<Row className="mb-5 d-flex flex-row justify-content-center">
<Col md={ 3 }>
<Link to={ '/n/features#Share-Notes' } className="text-light">
<ForkAwesomeIcon icon="bolt" size="3x"/>
<h5>
<ForkAwesomeIcon icon="bolt" size="2x"/>
<h6>
<Trans i18nKey="landing.intro.features.collaboration"/>
</h5>
</h6>
</Link>
</Col>
<Col md={ 4 }>
<Col md={ 3 }>
<Link to={ '/n/features#MathJax' } className="text-light">
<ForkAwesomeIcon icon="bar-chart" size="3x"/>
<h5>
<ForkAwesomeIcon icon="bar-chart" size="2x"/>
<h6>
<Trans i18nKey="landing.intro.features.katex"/>
</h5>
</h6>
</Link>
</Col>
<Col md={ 4 }>
<Col md={ 3 }>
<Link to={ '/n/features#Slide-Mode' } className="text-light">
<ForkAwesomeIcon icon="television" size="3x"/>
<h5>
<ForkAwesomeIcon icon="television" size="2x"/>
<h6>
<Trans i18nKey="landing.intro.features.slides"/>
</h5>
</h6>
</Link>
</Col>
</Row>

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getFrontPageContent } from '../requests'
import { useFrontendBaseUrl } from '../../../hooks/common/use-frontend-base-url'
const MARKDOWN_WHILE_LOADING = ':zzz: {message}'
const MARKDOWN_IF_ERROR = ':::danger\n' +
'{message}\n' +
':::'
export const useIntroPageContent = (): string => {
const { t } = useTranslation()
const [content, setContent] = useState<string>(() => MARKDOWN_WHILE_LOADING.replace('{message}', t('landing.intro.markdownWhileLoading')))
const frontendBaseUrl = useFrontendBaseUrl()
useEffect(() => {
getFrontPageContent(frontendBaseUrl)
.then((content) => setContent(content))
.catch(() => setContent(MARKDOWN_IF_ERROR.replace('{message}', t('landing.intro.markdownLoadingError'))))
}, [frontendBaseUrl, t])
return content
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

View file

@ -1,37 +1,54 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
*SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import React, { Fragment, useState } from 'react'
import { Trans } from 'react-i18next'
import { Branding } from '../common/branding/branding'
import {
HedgeDocLogoSize,
HedgeDocLogoType,
HedgeDocLogoWithText
} from '../common/hedge-doc-logo/hedge-doc-logo-with-text'
import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
import { CoverButtons } from './cover-buttons/cover-buttons'
import { FeatureLinks } from './feature-links'
import screenshot from './img/screenshot.png'
import { useIntroPageContent } from './hooks/use-intro-page-content'
import { ShowIf } from '../common/show-if/show-if'
import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
import { RendererType } from '../render-page/rendering-message'
export const IntroPage: React.FC = () => {
const { t } = useTranslation()
const introPageContent = useIntroPageContent()
const [showSpinner, setShowSpinner] = useState<boolean>(true)
return <Fragment>
<h1 dir='auto' className={ 'align-items-center d-flex justify-content-center flex-column' }>
<HedgeDocLogoWithText logoType={ HedgeDocLogoType.COLOR_VERTICAL } size={ HedgeDocLogoSize.BIG }/>
</h1>
<p className="lead">
<Trans i18nKey="app.slogan"/>
</p>
<div className={ 'mb-5' }>
<Branding delimiter={ false }/>
</div>
<CoverButtons/>
<img alt={ t('landing.intro.screenShotAltText') } src={ screenshot } className="img-fluid mb-5"/>
<FeatureLinks/>
</Fragment>
return (
<Fragment>
<div className={ 'flex-fill mt-3' }>
<h1 dir='auto' className={ 'align-items-center d-flex justify-content-center flex-column' }>
<HedgeDocLogoWithText logoType={ HedgeDocLogoType.COLOR_VERTICAL } size={ HedgeDocLogoSize.BIG }/>
</h1>
<p className="lead">
<Trans i18nKey="app.slogan"/>
</p>
<div className={ 'mb-5' }>
<Branding delimiter={ false }/>
</div>
<CoverButtons/>
<ShowIf condition={ showSpinner }>
<ForkAwesomeIcon icon={ 'spinner' } className={ 'fa-spin' }/>
</ShowIf>
<RenderIframe
frameClasses={ 'w-100 overflow-y-hidden' }
markdownContent={ introPageContent }
disableToc={ true }
onRendererReadyChange={ (rendererReady => setShowSpinner(!rendererReady)) }
rendererType={ RendererType.INTRO }
forcedDarkMode={ true }/>
<hr className={ 'mb-5' }/>
</div>
<FeatureLinks/>
</Fragment>)
}

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defaultFetchConfig, expectResponseCode } from '../../api/utils'
export const getFrontPageContent = async (baseUrl: string): Promise<string> => {
const response = await fetch(baseUrl + '/intro.md', {
...defaultFetchConfig,
method: 'GET'
})
expectResponseCode(response)
return await response.text()
}

View file

@ -1,7 +1,7 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import equal from 'fast-deep-equal'
@ -43,10 +43,10 @@ export const VersionInfo: React.FC = () => {
return (
<Fragment>
<Link id='version' to={ '#' } className={ 'text-light' } onClick={ handleShow }>
<Link data-cy={ 'show-version-modal' } to={ '#' } className={ 'text-light' } onClick={ handleShow }>
<Trans i18nKey={ 'landing.versionInfo.versionInfo' }/>
</Link>
<Modal id='versionModal' show={ show } onHide={ handleClose } animation={ true }>
<Modal data-cy={ 'version-modal' } show={ show } onHide={ handleClose } animation={ true }>
<Modal.Body className="text-dark">
<h3><Trans i18nKey={ 'landing.versionInfo.title' }/></h3>
<Row>
@ -55,7 +55,7 @@ export const VersionInfo: React.FC = () => {
</Row>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={ handleClose }>
<Button variant="secondary" onClick={ handleClose } data-cy={ 'close-version-modal-button' }>
<Trans i18nKey={ 'common.close' }/>
</Button>
</Modal.Footer>

View file

@ -1,7 +1,7 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
@ -13,9 +13,7 @@ import { LinkContainer } from 'react-router-bootstrap'
import { ApplicationState } from '../../../redux'
import { ShowIf } from '../../common/show-if/show-if'
type SignInButtonProps = {
className?: string
} & Omit<ButtonProps, 'href'>
export type SignInButtonProps = Omit<ButtonProps, 'href'>
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
const { t } = useTranslation()
@ -25,9 +23,9 @@ export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props })
<ShowIf condition={ anyAuthProviderActive }>
<LinkContainer to="/login" title={ t('login.signIn') }>
<Button
data-cy={ 'sign-in-button' }
variant={ variant || 'success' }
{ ...props }
>
{ ...props }>
<Trans i18nKey="login.signIn"/>
</Button>
</LinkContainer>

View file

@ -71,11 +71,13 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
toc => {
tocAst.current = toc
},
lineMarkers => {
currentLineMarkers.current = lineMarkers
}
onLineMarkerPositionChanged === undefined
? undefined
: lineMarkers => {
currentLineMarkers.current = lineMarkers
}
)).buildConfiguredMarkdownIt()
}, [onFrontmatterChange])
}, [onLineMarkerPositionChanged, onFrontmatterChange])
const clearFrontmatter = useCallback(() => {
hasNewYamlError.current = false

View file

@ -29,7 +29,7 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
private passYamlErrorState: (error: boolean) => void,
private onRawMeta: (rawMeta: RawNoteFrontmatter) => void,
private onToc: (toc: TocAst) => void,
private onLineMarkers: (lineMarkers: LineMarkers[]) => void
private onLineMarkers?: (lineMarkers: LineMarkers[]) => void
) {
super()
}
@ -58,8 +58,12 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
AsciinemaReplacer.markdownItPlugin,
highlightedCode,
quoteExtra,
(markdownIt) => documentToc(markdownIt, this.onToc),
(markdownIt) => lineNumberMarker(markdownIt, (lineMarkers) => this.onLineMarkers(lineMarkers))
)
(markdownIt) => documentToc(markdownIt, this.onToc))
if (this.onLineMarkers) {
const callback = this.onLineMarkers
this.configurations.push(
(markdownIt) => lineNumberMarker(markdownIt, (lineMarkers) => callback(lineMarkers))
)
}
}
}

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
}

View file

@ -21,7 +21,7 @@
}
body {
background-color: darken($dark, 8%);
background-color: $dark;
}
html {
@ -87,3 +87,7 @@ body {
.overflow-x-auto {
overflow-x: auto !important;
}
.overflow-y-hidden {
overflow-y: hidden !important;
}

View file

@ -6,6 +6,7 @@
$blue: #337ab7 !default;
$cyan: #5EB7E0 !default;
$dark: #222222 !default;
@import "../../node_modules/bootstrap/scss/functions";
@import "../../node_modules/bootstrap/scss/mixins";