mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-14 15:14:56 -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
|
@ -27,6 +27,10 @@ Files: public/index.html
|
||||||
Copyright: 2021 The HedgeDoc developers (see AUTHORS file)
|
Copyright: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
License: CC0-1.0
|
License: CC0-1.0
|
||||||
|
|
||||||
|
Files: public/intro.md
|
||||||
|
Copyright: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
License: CC0-1.0
|
||||||
|
|
||||||
Files: public/robots.txt
|
Files: public/robots.txt
|
||||||
Copyright: 2021 The HedgeDoc developers (see AUTHORS file)
|
Copyright: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
License: CC0-1.0
|
License: CC0-1.0
|
||||||
|
|
|
@ -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).
|
- Surround selected text with a link via shortcut (ctrl+k or cmd+k).
|
||||||
- A sidebar for menu options
|
- A sidebar for menu options
|
||||||
- Improved security by wrapping the markdown rendering into an iframe
|
- Improved security by wrapping the markdown rendering into an iframe
|
||||||
|
- The intro page content can be changed by editing `public/intro.md`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -5,50 +5,58 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
describe('Intro', () => {
|
describe('Intro page', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
cy.intercept('/intro.md', 'test content')
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Cover Button are hidden when logged in', () => {
|
describe('content', () => {
|
||||||
it('Sign in Cover Button', () => {
|
it('fetches and shows the correct intro page content', () => {
|
||||||
cy.get('.cover-button.btn-success')
|
cy.getMarkdownBody()
|
||||||
.should('not.exist')
|
.contains('test content')
|
||||||
})
|
|
||||||
|
|
||||||
it('Features Cover Button', () => {
|
|
||||||
cy.get('.cover-button.btn-primary')
|
|
||||||
.should('not.exist')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Cover Button are shown when logged out', () => {
|
describe('features button', () => {
|
||||||
beforeEach(() => {
|
it('is hidden when logged in', () => {
|
||||||
|
cy.get('[data-cy="features-button"]')
|
||||||
|
.should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is visible when logged out', () => {
|
||||||
cy.logout()
|
cy.logout()
|
||||||
})
|
cy.get('[data-cy="features-button"]')
|
||||||
|
|
||||||
it('Sign in Cover Button', () => {
|
|
||||||
cy.get('.cover-button.btn-success')
|
|
||||||
.should('exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Features Cover Button', () => {
|
|
||||||
cy.get('.cover-button.btn-primary')
|
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Version can be opened and closed', () => {
|
describe('sign in button', () => {
|
||||||
cy.get('#versionModal')
|
it('is hidden when logged in', () => {
|
||||||
.should('not.exist')
|
cy.get('[data-cy="sign-in-button"]')
|
||||||
cy.get('#version')
|
.should('not.exist')
|
||||||
.click()
|
})
|
||||||
cy.get('#versionModal')
|
|
||||||
.should('be.visible')
|
it('is visible when logged out', () => {
|
||||||
cy.get('#versionModal .modal-footer .btn')
|
cy.logout()
|
||||||
.contains('Close')
|
cy.get('[data-cy="sign-in-button"]')
|
||||||
.click()
|
.should('exist')
|
||||||
cy.get('#versionModal')
|
})
|
||||||
.should('not.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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -94,21 +94,21 @@ describe('Links Intro', () => {
|
||||||
|
|
||||||
describe('Feature Links', () => {
|
describe('Feature Links', () => {
|
||||||
it('Share-Notes', () => {
|
it('Share-Notes', () => {
|
||||||
cy.get('i.fa-bolt.fa-3x')
|
cy.get('i.fa-bolt')
|
||||||
.click()
|
.click()
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('include', '/features#Share-Notes')
|
.should('include', '/features#Share-Notes')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('KaTeX', () => {
|
it('KaTeX', () => {
|
||||||
cy.get('i.fa-bar-chart.fa-3x')
|
cy.get('i.fa-bar-chart')
|
||||||
.click()
|
.click()
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('include', '/features#MathJax')
|
.should('include', '/features#MathJax')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Slide-Mode', () => {
|
it('Slide-Mode', () => {
|
||||||
cy.get('i.fa-television.fa-3x')
|
cy.get('i.fa-television')
|
||||||
.click()
|
.click()
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('include', '/features#Slide-Mode')
|
.should('include', '/features#Slide-Mode')
|
||||||
|
|
|
@ -14,6 +14,7 @@ declare namespace Cypress {
|
||||||
|
|
||||||
Cypress.Commands.add('getMarkdownRenderer', () => {
|
Cypress.Commands.add('getMarkdownRenderer', () => {
|
||||||
return cy.get(`iframe[data-cy="documentIframe"]`)
|
return cy.get(`iframe[data-cy="documentIframe"]`)
|
||||||
|
.should('be.visible')
|
||||||
.its('0.contentDocument')
|
.its('0.contentDocument')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
.its('body')
|
.its('body')
|
||||||
|
|
5
public/intro.md
Normal file
5
public/intro.md
Normal 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.
|
||||||
|
:::
|
||||||
|
|
||||||
|

|
|
@ -34,7 +34,8 @@
|
||||||
"katex": "Works with charts and KaTeX",
|
"katex": "Works with charts and KaTeX",
|
||||||
"slides": "Supports slide mode"
|
"slides": "Supports slide mode"
|
||||||
},
|
},
|
||||||
"screenShotAltText": "HedgeDoc Screenshot"
|
"markdownWhileLoading": "Loading...",
|
||||||
|
"markdownLoadingError": "Error while fetching intro content"
|
||||||
},
|
},
|
||||||
"history": {
|
"history": {
|
||||||
"error": {
|
"error": {
|
||||||
|
|
BIN
public/screenshot.png
Normal file
BIN
public/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 250 KiB |
|
@ -22,6 +22,7 @@ import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
|
||||||
import { DocumentInfobar } from './document-infobar'
|
import { DocumentInfobar } from './document-infobar'
|
||||||
import { ErrorWhileLoadingNoteAlert } from './ErrorWhileLoadingNoteAlert'
|
import { ErrorWhileLoadingNoteAlert } from './ErrorWhileLoadingNoteAlert'
|
||||||
import { LoadingNoteAlert } from './LoadingNoteAlert'
|
import { LoadingNoteAlert } from './LoadingNoteAlert'
|
||||||
|
import { RendererType } from '../render-page/rendering-message'
|
||||||
|
|
||||||
export const DocumentReadOnlyPage: React.FC = () => {
|
export const DocumentReadOnlyPage: React.FC = () => {
|
||||||
|
|
||||||
|
@ -55,10 +56,11 @@ export const DocumentReadOnlyPage: React.FC = () => {
|
||||||
noteId={ id }
|
noteId={ id }
|
||||||
viewCount={ noteDetails.viewCount }
|
viewCount={ noteDetails.viewCount }
|
||||||
/>
|
/>
|
||||||
<RenderIframe extraClasses={ 'flex-fill' }
|
<RenderIframe frameClasses={ 'flex-fill h-100 w-100' }
|
||||||
markdownContent={ markdownContent }
|
markdownContent={ markdownContent }
|
||||||
onFirstHeadingChange={ onFirstHeadingChange }
|
onFirstHeadingChange={ onFirstHeadingChange }
|
||||||
onFrontmatterChange={ onFrontmatterChange }/>
|
onFrontmatterChange={ onFrontmatterChange }
|
||||||
|
rendererType={RendererType.DOCUMENT}/>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { RenderIframe } from './renderer-pane/render-iframe'
|
||||||
import { Sidebar } from './sidebar/sidebar'
|
import { Sidebar } from './sidebar/sidebar'
|
||||||
import { Splitter } from './splitter/splitter'
|
import { Splitter } from './splitter/splitter'
|
||||||
import { DualScrollState, ScrollState } from './synced-scroll/scroll-props'
|
import { DualScrollState, ScrollState } from './synced-scroll/scroll-props'
|
||||||
|
import { RendererType } from '../render-page/rendering-message'
|
||||||
|
|
||||||
export interface EditorPagePathParams {
|
export interface EditorPagePathParams {
|
||||||
id: string
|
id: string
|
||||||
|
@ -115,13 +116,15 @@ export const EditorPage: React.FC = () => {
|
||||||
showRight={ editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH }
|
showRight={ editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH }
|
||||||
right={
|
right={
|
||||||
<RenderIframe
|
<RenderIframe
|
||||||
|
frameClasses={'h-100 w-100'}
|
||||||
markdownContent={ markdownContent }
|
markdownContent={ markdownContent }
|
||||||
onMakeScrollSource={ setRendererToScrollSource }
|
onMakeScrollSource={ setRendererToScrollSource }
|
||||||
onFirstHeadingChange={ updateNoteTitleByFirstHeading }
|
onFirstHeadingChange={ updateNoteTitleByFirstHeading }
|
||||||
onTaskCheckedChange={ SetCheckboxInMarkdownContent }
|
onTaskCheckedChange={ SetCheckboxInMarkdownContent }
|
||||||
onFrontmatterChange={ setNoteFrontmatter }
|
onFrontmatterChange={ setNoteFrontmatter }
|
||||||
onScroll={ onMarkdownRendererScroll }
|
onScroll={ onMarkdownRendererScroll }
|
||||||
scrollState={ scrollState.rendererScrollState }/>
|
scrollState={ scrollState.rendererScrollState }
|
||||||
|
rendererType={ RendererType.DOCUMENT }/>
|
||||||
}
|
}
|
||||||
containerClassName={ 'overflow-hidden' }/>
|
containerClassName={ 'overflow-hidden' }/>
|
||||||
<Sidebar/>
|
<Sidebar/>
|
||||||
|
|
|
@ -10,13 +10,20 @@ import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-a
|
||||||
import { ApplicationState } from '../../../redux'
|
import { ApplicationState } from '../../../redux'
|
||||||
import { isTestMode } from '../../../utils/is-test-mode'
|
import { isTestMode } from '../../../utils/is-test-mode'
|
||||||
import { IframeEditorToRendererCommunicator } from '../../render-page/iframe-editor-to-renderer-communicator'
|
import { IframeEditorToRendererCommunicator } from '../../render-page/iframe-editor-to-renderer-communicator'
|
||||||
import { MarkdownDocumentProps } from '../../render-page/markdown-document'
|
import { RendererProps } from '../../render-page/markdown-document'
|
||||||
import { ImageDetails } from '../../render-page/rendering-message'
|
import { ImageDetails, RendererType } from '../../render-page/rendering-message'
|
||||||
import { ScrollState } from '../synced-scroll/scroll-props'
|
import { ScrollState } from '../synced-scroll/scroll-props'
|
||||||
import { useOnIframeLoad } from './hooks/use-on-iframe-load'
|
import { useOnIframeLoad } from './hooks/use-on-iframe-load'
|
||||||
import { ShowOnPropChangeImageLightbox } from './show-on-prop-change-image-lightbox'
|
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,
|
markdownContent,
|
||||||
onTaskCheckedChange,
|
onTaskCheckedChange,
|
||||||
|
@ -25,9 +32,13 @@ export const RenderIframe: React.FC<MarkdownDocumentProps> = (
|
||||||
onFirstHeadingChange,
|
onFirstHeadingChange,
|
||||||
onScroll,
|
onScroll,
|
||||||
onMakeScrollSource,
|
onMakeScrollSource,
|
||||||
extraClasses
|
frameClasses,
|
||||||
|
onRendererReadyChange,
|
||||||
|
rendererType,
|
||||||
|
forcedDarkMode
|
||||||
}) => {
|
}) => {
|
||||||
const darkMode = useIsDarkModeActivated()
|
const savedDarkMode = useIsDarkModeActivated()
|
||||||
|
const darkMode = forcedDarkMode ?? savedDarkMode
|
||||||
const [rendererReady, setRendererReady] = useState<boolean>(false)
|
const [rendererReady, setRendererReady] = useState<boolean>(false)
|
||||||
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
|
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
|
||||||
|
|
||||||
|
@ -37,6 +48,11 @@ export const RenderIframe: React.FC<MarkdownDocumentProps> = (
|
||||||
const resetRendererReady = useCallback(() => setRendererReady(false), [])
|
const resetRendererReady = useCallback(() => setRendererReady(false), [])
|
||||||
const iframeCommunicator = useMemo(() => new IframeEditorToRendererCommunicator(), [])
|
const iframeCommunicator = useMemo(() => new IframeEditorToRendererCommunicator(), [])
|
||||||
const onIframeLoad = useOnIframeLoad(frameReference, iframeCommunicator, rendererOrigin, renderPageUrl, resetRendererReady)
|
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.unregisterEventListener(), [iframeCommunicator])
|
||||||
useEffect(() => iframeCommunicator.onFirstHeadingChange(onFirstHeadingChange), [iframeCommunicator,
|
useEffect(() => iframeCommunicator.onFirstHeadingChange(onFirstHeadingChange), [iframeCommunicator,
|
||||||
|
@ -49,8 +65,15 @@ export const RenderIframe: React.FC<MarkdownDocumentProps> = (
|
||||||
useEffect(() => iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange), [iframeCommunicator,
|
useEffect(() => iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange), [iframeCommunicator,
|
||||||
onTaskCheckedChange])
|
onTaskCheckedChange])
|
||||||
useEffect(() => iframeCommunicator.onImageClicked(setLightboxDetails), [iframeCommunicator])
|
useEffect(() => iframeCommunicator.onImageClicked(setLightboxDetails), [iframeCommunicator])
|
||||||
useEffect(() => iframeCommunicator.onRendererReady(() => setRendererReady(true)), [darkMode, iframeCommunicator,
|
useEffect(() => iframeCommunicator.onRendererReady(() => {
|
||||||
scrollState])
|
iframeCommunicator.sendSetBaseConfiguration({
|
||||||
|
baseUrl: window.location.toString(),
|
||||||
|
rendererType
|
||||||
|
})
|
||||||
|
setRendererReady(true)
|
||||||
|
}), [darkMode, rendererType, iframeCommunicator, rendererReady, scrollState])
|
||||||
|
useEffect(() => iframeCommunicator.onHeightChange(setFrameHeight), [iframeCommunicator])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rendererReady) {
|
if (rendererReady) {
|
||||||
iframeCommunicator.sendSetDarkmode(darkMode)
|
iframeCommunicator.sendSetDarkmode(darkMode)
|
||||||
|
@ -65,12 +88,6 @@ export const RenderIframe: React.FC<MarkdownDocumentProps> = (
|
||||||
}
|
}
|
||||||
}, [iframeCommunicator, rendererReady, scrollState])
|
}, [iframeCommunicator, rendererReady, scrollState])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (rendererReady) {
|
|
||||||
iframeCommunicator.sendSetBaseUrl(window.location.toString())
|
|
||||||
}
|
|
||||||
}, [iframeCommunicator, rendererReady])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rendererReady) {
|
if (rendererReady) {
|
||||||
iframeCommunicator.sendSetMarkdownContent(markdownContent)
|
iframeCommunicator.sendSetMarkdownContent(markdownContent)
|
||||||
|
@ -79,8 +96,9 @@ export const RenderIframe: React.FC<MarkdownDocumentProps> = (
|
||||||
|
|
||||||
return <Fragment>
|
return <Fragment>
|
||||||
<ShowOnPropChangeImageLightbox details={ lightboxDetails }/>
|
<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' } }
|
{ ...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>
|
</Fragment>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
@ -38,6 +38,7 @@ export const CoverButtons: React.FC = () => {
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<Link to="/n/features">
|
<Link to="/n/features">
|
||||||
<Button
|
<Button
|
||||||
|
data-cy={ 'features-button' }
|
||||||
className="cover-button"
|
className="cover-button"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|
|
@ -13,29 +13,29 @@ import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
|
||||||
export const FeatureLinks: React.FC = () => {
|
export const FeatureLinks: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
return (
|
return (
|
||||||
<Row className="mb-5">
|
<Row className="mb-5 d-flex flex-row justify-content-center">
|
||||||
<Col md={ 4 }>
|
<Col md={ 3 }>
|
||||||
<Link to={ '/n/features#Share-Notes' } className="text-light">
|
<Link to={ '/n/features#Share-Notes' } className="text-light">
|
||||||
<ForkAwesomeIcon icon="bolt" size="3x"/>
|
<ForkAwesomeIcon icon="bolt" size="2x"/>
|
||||||
<h5>
|
<h6>
|
||||||
<Trans i18nKey="landing.intro.features.collaboration"/>
|
<Trans i18nKey="landing.intro.features.collaboration"/>
|
||||||
</h5>
|
</h6>
|
||||||
</Link>
|
</Link>
|
||||||
</Col>
|
</Col>
|
||||||
<Col md={ 4 }>
|
<Col md={ 3 }>
|
||||||
<Link to={ '/n/features#MathJax' } className="text-light">
|
<Link to={ '/n/features#MathJax' } className="text-light">
|
||||||
<ForkAwesomeIcon icon="bar-chart" size="3x"/>
|
<ForkAwesomeIcon icon="bar-chart" size="2x"/>
|
||||||
<h5>
|
<h6>
|
||||||
<Trans i18nKey="landing.intro.features.katex"/>
|
<Trans i18nKey="landing.intro.features.katex"/>
|
||||||
</h5>
|
</h6>
|
||||||
</Link>
|
</Link>
|
||||||
</Col>
|
</Col>
|
||||||
<Col md={ 4 }>
|
<Col md={ 3 }>
|
||||||
<Link to={ '/n/features#Slide-Mode' } className="text-light">
|
<Link to={ '/n/features#Slide-Mode' } className="text-light">
|
||||||
<ForkAwesomeIcon icon="television" size="3x"/>
|
<ForkAwesomeIcon icon="television" size="2x"/>
|
||||||
<h5>
|
<h6>
|
||||||
<Trans i18nKey="landing.intro.features.slides"/>
|
<Trans i18nKey="landing.intro.features.slides"/>
|
||||||
</h5>
|
</h6>
|
||||||
</Link>
|
</Link>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
29
src/components/intro-page/hooks/use-intro-page-content.ts
Normal file
29
src/components/intro-page/hooks/use-intro-page-content.ts
Normal 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 |
|
@ -1,37 +1,54 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
*SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment, useState } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
import { Branding } from '../common/branding/branding'
|
import { Branding } from '../common/branding/branding'
|
||||||
import {
|
import {
|
||||||
HedgeDocLogoSize,
|
HedgeDocLogoSize,
|
||||||
HedgeDocLogoType,
|
HedgeDocLogoType,
|
||||||
HedgeDocLogoWithText
|
HedgeDocLogoWithText
|
||||||
} from '../common/hedge-doc-logo/hedge-doc-logo-with-text'
|
} 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 { CoverButtons } from './cover-buttons/cover-buttons'
|
||||||
import { FeatureLinks } from './feature-links'
|
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 = () => {
|
export const IntroPage: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const introPageContent = useIntroPageContent()
|
||||||
|
const [showSpinner, setShowSpinner] = useState<boolean>(true)
|
||||||
|
|
||||||
return <Fragment>
|
return (
|
||||||
<h1 dir='auto' className={ 'align-items-center d-flex justify-content-center flex-column' }>
|
<Fragment>
|
||||||
<HedgeDocLogoWithText logoType={ HedgeDocLogoType.COLOR_VERTICAL } size={ HedgeDocLogoSize.BIG }/>
|
<div className={ 'flex-fill mt-3' }>
|
||||||
</h1>
|
<h1 dir='auto' className={ 'align-items-center d-flex justify-content-center flex-column' }>
|
||||||
<p className="lead">
|
<HedgeDocLogoWithText logoType={ HedgeDocLogoType.COLOR_VERTICAL } size={ HedgeDocLogoSize.BIG }/>
|
||||||
<Trans i18nKey="app.slogan"/>
|
</h1>
|
||||||
</p>
|
<p className="lead">
|
||||||
<div className={ 'mb-5' }>
|
<Trans i18nKey="app.slogan"/>
|
||||||
<Branding delimiter={ false }/>
|
</p>
|
||||||
</div>
|
<div className={ 'mb-5' }>
|
||||||
|
<Branding delimiter={ false }/>
|
||||||
<CoverButtons/>
|
</div>
|
||||||
<img alt={ t('landing.intro.screenShotAltText') } src={ screenshot } className="img-fluid mb-5"/>
|
<CoverButtons/>
|
||||||
<FeatureLinks/>
|
<ShowIf condition={ showSpinner }>
|
||||||
</Fragment>
|
<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>)
|
||||||
}
|
}
|
||||||
|
|
17
src/components/intro-page/requests.ts
Normal file
17
src/components/intro-page/requests.ts
Normal 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()
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import equal from 'fast-deep-equal'
|
import equal from 'fast-deep-equal'
|
||||||
|
@ -43,10 +43,10 @@ export const VersionInfo: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<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' }/>
|
<Trans i18nKey={ 'landing.versionInfo.versionInfo' }/>
|
||||||
</Link>
|
</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">
|
<Modal.Body className="text-dark">
|
||||||
<h3><Trans i18nKey={ 'landing.versionInfo.title' }/></h3>
|
<h3><Trans i18nKey={ 'landing.versionInfo.title' }/></h3>
|
||||||
<Row>
|
<Row>
|
||||||
|
@ -55,7 +55,7 @@ export const VersionInfo: React.FC = () => {
|
||||||
</Row>
|
</Row>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button variant="secondary" onClick={ handleClose }>
|
<Button variant="secondary" onClick={ handleClose } data-cy={ 'close-version-modal-button' }>
|
||||||
<Trans i18nKey={ 'common.close' }/>
|
<Trans i18nKey={ 'common.close' }/>
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
@ -13,9 +13,7 @@ import { LinkContainer } from 'react-router-bootstrap'
|
||||||
import { ApplicationState } from '../../../redux'
|
import { ApplicationState } from '../../../redux'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
|
|
||||||
type SignInButtonProps = {
|
export type SignInButtonProps = Omit<ButtonProps, 'href'>
|
||||||
className?: string
|
|
||||||
} & Omit<ButtonProps, 'href'>
|
|
||||||
|
|
||||||
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
|
export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
@ -25,9 +23,9 @@ export const SignInButton: React.FC<SignInButtonProps> = ({ variant, ...props })
|
||||||
<ShowIf condition={ anyAuthProviderActive }>
|
<ShowIf condition={ anyAuthProviderActive }>
|
||||||
<LinkContainer to="/login" title={ t('login.signIn') }>
|
<LinkContainer to="/login" title={ t('login.signIn') }>
|
||||||
<Button
|
<Button
|
||||||
|
data-cy={ 'sign-in-button' }
|
||||||
variant={ variant || 'success' }
|
variant={ variant || 'success' }
|
||||||
{ ...props }
|
{ ...props }>
|
||||||
>
|
|
||||||
<Trans i18nKey="login.signIn"/>
|
<Trans i18nKey="login.signIn"/>
|
||||||
</Button>
|
</Button>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
|
|
|
@ -71,11 +71,13 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
|
||||||
toc => {
|
toc => {
|
||||||
tocAst.current = toc
|
tocAst.current = toc
|
||||||
},
|
},
|
||||||
lineMarkers => {
|
onLineMarkerPositionChanged === undefined
|
||||||
currentLineMarkers.current = lineMarkers
|
? undefined
|
||||||
}
|
: lineMarkers => {
|
||||||
|
currentLineMarkers.current = lineMarkers
|
||||||
|
}
|
||||||
)).buildConfiguredMarkdownIt()
|
)).buildConfiguredMarkdownIt()
|
||||||
}, [onFrontmatterChange])
|
}, [onLineMarkerPositionChanged, onFrontmatterChange])
|
||||||
|
|
||||||
const clearFrontmatter = useCallback(() => {
|
const clearFrontmatter = useCallback(() => {
|
||||||
hasNewYamlError.current = false
|
hasNewYamlError.current = false
|
||||||
|
|
|
@ -29,7 +29,7 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
|
||||||
private passYamlErrorState: (error: boolean) => void,
|
private passYamlErrorState: (error: boolean) => void,
|
||||||
private onRawMeta: (rawMeta: RawNoteFrontmatter) => void,
|
private onRawMeta: (rawMeta: RawNoteFrontmatter) => void,
|
||||||
private onToc: (toc: TocAst) => void,
|
private onToc: (toc: TocAst) => void,
|
||||||
private onLineMarkers: (lineMarkers: LineMarkers[]) => void
|
private onLineMarkers?: (lineMarkers: LineMarkers[]) => void
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
@ -58,8 +58,12 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
|
||||||
AsciinemaReplacer.markdownItPlugin,
|
AsciinemaReplacer.markdownItPlugin,
|
||||||
highlightedCode,
|
highlightedCode,
|
||||||
quoteExtra,
|
quoteExtra,
|
||||||
(markdownIt) => documentToc(markdownIt, this.onToc),
|
(markdownIt) => documentToc(markdownIt, this.onToc))
|
||||||
(markdownIt) => lineNumberMarker(markdownIt, (lineMarkers) => this.onLineMarkers(lineMarkers))
|
if (this.onLineMarkers) {
|
||||||
)
|
const callback = this.onLineMarkers
|
||||||
|
this.configurations.push(
|
||||||
|
(markdownIt) => lineNumberMarker(markdownIt, (lineMarkers) => callback(lineMarkers))
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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 { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import { IframeCommunicator } from './iframe-communicator'
|
import { IframeCommunicator } from './iframe-communicator'
|
||||||
import {
|
import {
|
||||||
|
BaseConfiguration,
|
||||||
EditorToRendererIframeMessage,
|
EditorToRendererIframeMessage,
|
||||||
ImageDetails,
|
ImageDetails,
|
||||||
RendererToEditorIframeMessage,
|
RendererToEditorIframeMessage,
|
||||||
|
@ -22,6 +23,11 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<Edito
|
||||||
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
|
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
|
||||||
private onRendererReadyHandler?: () => void
|
private onRendererReadyHandler?: () => void
|
||||||
private onImageClickedHandler?: (details: ImageDetails) => 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 {
|
public onFrontmatterChange(handler?: (frontmatter?: NoteFrontmatter) => void): void {
|
||||||
this.onFrontmatterChangeHandler = handler
|
this.onFrontmatterChangeHandler = handler
|
||||||
|
@ -51,10 +57,10 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<Edito
|
||||||
this.onSetScrollStateHandler = handler
|
this.onSetScrollStateHandler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendSetBaseUrl(baseUrl: string): void {
|
public sendSetBaseConfiguration(baseConfiguration: BaseConfiguration): void {
|
||||||
this.sendMessageToOtherSide({
|
this.sendMessageToOtherSide({
|
||||||
type: RenderIframeMessageType.SET_BASE_URL,
|
type: RenderIframeMessageType.SET_BASE_CONFIGURATION,
|
||||||
baseUrl
|
baseConfiguration
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,6 +112,9 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<Edito
|
||||||
case RenderIframeMessageType.IMAGE_CLICKED:
|
case RenderIframeMessageType.IMAGE_CLICKED:
|
||||||
this.onImageClickedHandler?.(renderMessage.details)
|
this.onImageClickedHandler?.(renderMessage.details)
|
||||||
return false
|
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 { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import { IframeCommunicator } from './iframe-communicator'
|
import { IframeCommunicator } from './iframe-communicator'
|
||||||
import {
|
import {
|
||||||
|
BaseConfiguration,
|
||||||
EditorToRendererIframeMessage,
|
EditorToRendererIframeMessage,
|
||||||
ImageDetails,
|
ImageDetails,
|
||||||
RendererToEditorIframeMessage,
|
RendererToEditorIframeMessage,
|
||||||
|
@ -18,10 +19,10 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<Rende
|
||||||
private onSetMarkdownContentHandler?: ((markdownContent: string) => void)
|
private onSetMarkdownContentHandler?: ((markdownContent: string) => void)
|
||||||
private onSetDarkModeHandler?: ((darkModeActivated: boolean) => void)
|
private onSetDarkModeHandler?: ((darkModeActivated: boolean) => void)
|
||||||
private onSetScrollStateHandler?: ((scrollState: ScrollState) => void)
|
private onSetScrollStateHandler?: ((scrollState: ScrollState) => void)
|
||||||
private onSetBaseUrlHandler?: ((baseUrl: string) => void)
|
private onSetBaseConfigurationHandler?: ((baseConfiguration: BaseConfiguration) => void)
|
||||||
|
|
||||||
public onSetBaseUrl(handler?: (baseUrl: string) => void): void {
|
public onSetBaseConfiguration(handler?: (baseConfiguration: BaseConfiguration) => void): void {
|
||||||
this.onSetBaseUrlHandler = handler
|
this.onSetBaseConfigurationHandler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSetMarkdownContent(handler?: (markdownContent: string) => void): void {
|
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 {
|
protected handleEvent(event: MessageEvent<EditorToRendererIframeMessage>): boolean | undefined {
|
||||||
const renderMessage = event.data
|
const renderMessage = event.data
|
||||||
switch (renderMessage.type) {
|
switch (renderMessage.type) {
|
||||||
|
@ -96,8 +104,8 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<Rende
|
||||||
case RenderIframeMessageType.SET_SCROLL_STATE:
|
case RenderIframeMessageType.SET_SCROLL_STATE:
|
||||||
this.onSetScrollStateHandler?.(renderMessage.scrollState)
|
this.onSetScrollStateHandler?.(renderMessage.scrollState)
|
||||||
return false
|
return false
|
||||||
case RenderIframeMessageType.SET_BASE_URL:
|
case RenderIframeMessageType.SET_BASE_CONFIGURATION:
|
||||||
this.onSetBaseUrlHandler?.(renderMessage.baseUrl)
|
this.onSetBaseConfigurationHandler?.(renderMessage.baseConfiguration)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow-y: scroll;
|
overflow: auto;
|
||||||
overflow-x: auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TocAst } from 'markdown-it-toc-done-right'
|
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 { Dropdown } from 'react-bootstrap'
|
||||||
import useResizeObserver from 'use-resize-observer'
|
import useResizeObserver from 'use-resize-observer'
|
||||||
import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
|
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 { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
||||||
import './markdown-document.scss'
|
import './markdown-document.scss'
|
||||||
|
|
||||||
export interface MarkdownDocumentProps extends ScrollProps {
|
export interface RendererProps extends ScrollProps {
|
||||||
extraClasses?: string
|
|
||||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||||
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
|
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
|
||||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||||
|
@ -28,11 +27,19 @@ export interface MarkdownDocumentProps extends ScrollProps {
|
||||||
markdownContent: string,
|
markdownContent: string,
|
||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
onImageClick?: ImageClickHandler
|
onImageClick?: ImageClickHandler
|
||||||
|
onHeightChange?: (height: number) => void
|
||||||
|
disableToc?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkdownDocumentProps extends RendererProps {
|
||||||
|
additionalOuterContainerClasses?: string
|
||||||
|
additionalRendererClasses?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MarkdownDocument: React.FC<MarkdownDocumentProps> = (
|
export const MarkdownDocument: React.FC<MarkdownDocumentProps> = (
|
||||||
{
|
{
|
||||||
extraClasses,
|
additionalOuterContainerClasses,
|
||||||
|
additionalRendererClasses,
|
||||||
onFirstHeadingChange,
|
onFirstHeadingChange,
|
||||||
onFrontmatterChange,
|
onFrontmatterChange,
|
||||||
onMakeScrollSource,
|
onMakeScrollSource,
|
||||||
|
@ -41,42 +48,53 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = (
|
||||||
markdownContent,
|
markdownContent,
|
||||||
onImageClick,
|
onImageClick,
|
||||||
onScroll,
|
onScroll,
|
||||||
scrollState
|
scrollState,
|
||||||
|
onHeightChange,
|
||||||
|
disableToc
|
||||||
}) => {
|
}) => {
|
||||||
const rendererRef = useRef<HTMLDivElement | null>(null)
|
const rendererRef = useRef<HTMLDivElement | null>(null)
|
||||||
const internalDocumentRenderPaneRef = useRef<HTMLDivElement>(null)
|
const internalDocumentRenderPaneRef = useRef<HTMLDivElement>(null)
|
||||||
const [tocAst, setTocAst] = useState<TocAst>()
|
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 contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent])
|
||||||
const [onLineMarkerPositionChanged, onUserScroll] = useSyncedScrolling(internalDocumentRenderPaneRef, rendererRef, contentLineCount, scrollState, onScroll)
|
const [onLineMarkerPositionChanged, onUserScroll] = useSyncedScrolling(internalDocumentRenderPaneRef, rendererRef, contentLineCount, scrollState, onScroll)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ `markdown-document ${ extraClasses ?? '' }` }
|
<div className={ `markdown-document ${ additionalOuterContainerClasses ?? '' }` }
|
||||||
ref={ internalDocumentRenderPaneRef } onScroll={ onUserScroll } onMouseEnter={ onMakeScrollSource }>
|
ref={ internalDocumentRenderPaneRef } onScroll={ onUserScroll } onMouseEnter={ onMakeScrollSource }>
|
||||||
<div className={ 'markdown-document-side' }/>
|
<div className={ 'markdown-document-side' }/>
|
||||||
<div className={ 'bg-light markdown-document-content' }>
|
<div className={ 'markdown-document-content' }>
|
||||||
<YamlArrayDeprecationAlert/>
|
<YamlArrayDeprecationAlert/>
|
||||||
<FullMarkdownRenderer
|
<FullMarkdownRenderer
|
||||||
rendererRef={ rendererRef }
|
rendererRef={ rendererRef }
|
||||||
className={ 'flex-fill pt-4 mb-3' }
|
className={ `flex-fill mb-3 ${ additionalRendererClasses ?? '' }` }
|
||||||
content={ markdownContent }
|
content={ markdownContent }
|
||||||
onFirstHeadingChange={ onFirstHeadingChange }
|
onFirstHeadingChange={ onFirstHeadingChange }
|
||||||
onLineMarkerPositionChanged={ onLineMarkerPositionChanged }
|
onLineMarkerPositionChanged={ onLineMarkerPositionChanged }
|
||||||
onFrontmatterChange={ onFrontmatterChange }
|
onFrontmatterChange={ onFrontmatterChange }
|
||||||
onTaskCheckedChange={ onTaskCheckedChange }
|
onTaskCheckedChange={ onTaskCheckedChange }
|
||||||
onTocChange={ (tocAst) => setTocAst(tocAst) }
|
onTocChange={ setTocAst }
|
||||||
baseUrl={ baseUrl }
|
baseUrl={ baseUrl }
|
||||||
onImageClick={ onImageClick }/>
|
onImageClick={ onImageClick }/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={ 'markdown-document-side pt-4' }>
|
<div className={ 'markdown-document-side pt-4' }>
|
||||||
<ShowIf condition={ !!tocAst }>
|
<ShowIf condition={ !!tocAst && !disableToc }>
|
||||||
<ShowIf condition={ width >= 1100 }>
|
<ShowIf condition={ containerWidth >= 1100 }>
|
||||||
<TableOfContents ast={ tocAst as TocAst } className={ 'sticky' } baseUrl={ baseUrl }/>
|
<TableOfContents ast={ tocAst as TocAst } className={ 'sticky' } baseUrl={ baseUrl }/>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={ width < 1100 }>
|
<ShowIf condition={ containerWidth < 1100 }>
|
||||||
<div className={ 'markdown-toc-sidebar-button' }>
|
<div className={ 'markdown-toc-sidebar-button' }>
|
||||||
<Dropdown drop={ 'up' }>
|
<Dropdown drop={ 'up' }>
|
||||||
<Dropdown.Toggle id="toc-overlay-button" variant={ 'secondary' } className={ 'no-arrow' }>
|
<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 { NoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
||||||
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
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 { IframeRendererToEditorCommunicator } from './iframe-renderer-to-editor-communicator'
|
||||||
import { MarkdownDocument } from './markdown-document'
|
import { MarkdownDocument } from './markdown-document'
|
||||||
|
import { BaseConfiguration, RendererType } from './rendering-message'
|
||||||
|
|
||||||
export const RenderPage: React.FC = () => {
|
export const RenderPage: React.FC = () => {
|
||||||
useApplyDarkMode()
|
useApplyDarkMode()
|
||||||
|
|
||||||
const [markdownContent, setMarkdownContent] = useState('')
|
const [markdownContent, setMarkdownContent] = useState('')
|
||||||
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
|
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)
|
const editorOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.editorOrigin)
|
||||||
|
|
||||||
|
@ -35,7 +37,7 @@ export const RenderPage: React.FC = () => {
|
||||||
return () => iframeCommunicator.unregisterEventListener()
|
return () => iframeCommunicator.unregisterEventListener()
|
||||||
}, [iframeCommunicator])
|
}, [iframeCommunicator])
|
||||||
|
|
||||||
useEffect(() => iframeCommunicator.onSetBaseUrl(setBaseUrl), [iframeCommunicator])
|
useEffect(() => iframeCommunicator.onSetBaseConfiguration(setBaseConfiguration), [iframeCommunicator])
|
||||||
useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
|
useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
|
||||||
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
|
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
|
||||||
useEffect(() => iframeCommunicator.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
|
useEffect(() => iframeCommunicator.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
|
||||||
|
@ -61,37 +63,45 @@ export const RenderPage: React.FC = () => {
|
||||||
iframeCommunicator.sendSetScrollState(scrollState)
|
iframeCommunicator.sendSetScrollState(scrollState)
|
||||||
}, [iframeCommunicator])
|
}, [iframeCommunicator])
|
||||||
|
|
||||||
const onImageClick: ImageClickHandler = useCallback((event) => {
|
const onImageClick: ImageClickHandler = useImageClickHandler(iframeCommunicator)
|
||||||
const image = event.target as HTMLImageElement
|
|
||||||
if (image.src === '') {
|
const onHeightChange = useCallback((height: number) => {
|
||||||
return
|
iframeCommunicator.sendHeightChange(height)
|
||||||
}
|
|
||||||
iframeCommunicator.sendClickedImageUrl({
|
|
||||||
src: image.src,
|
|
||||||
alt: image.alt,
|
|
||||||
title: image.title
|
|
||||||
})
|
|
||||||
}, [iframeCommunicator])
|
}, [iframeCommunicator])
|
||||||
|
|
||||||
if (!baseUrl) {
|
if (!baseConfiguration) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
switch (baseConfiguration.rendererType) {
|
||||||
<div className={ 'vh-100 w-100' }>
|
case RendererType.DOCUMENT:
|
||||||
<MarkdownDocument
|
return (
|
||||||
extraClasses={ 'bg-light' }
|
<MarkdownDocument
|
||||||
markdownContent={ markdownContent }
|
additionalOuterContainerClasses={ 'vh-100 bg-light' }
|
||||||
onTaskCheckedChange={ onTaskCheckedChange }
|
additionalRendererClasses={ 'mb-3' }
|
||||||
onFirstHeadingChange={ onFirstHeadingChange }
|
markdownContent={ markdownContent }
|
||||||
onMakeScrollSource={ onMakeScrollSource }
|
onTaskCheckedChange={ onTaskCheckedChange }
|
||||||
onFrontmatterChange={ onFrontmatterChange }
|
onFirstHeadingChange={ onFirstHeadingChange }
|
||||||
scrollState={ scrollState }
|
onMakeScrollSource={ onMakeScrollSource }
|
||||||
onScroll={ onScroll }
|
onFrontmatterChange={ onFrontmatterChange }
|
||||||
baseUrl={ baseUrl }
|
scrollState={ scrollState }
|
||||||
onImageClick={ onImageClick }/>
|
onScroll={ onScroll }
|
||||||
</div>
|
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
|
export default RenderPage
|
||||||
|
|
|
@ -16,7 +16,8 @@ export enum RenderIframeMessageType {
|
||||||
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
||||||
ON_SET_FRONTMATTER = 'ON_SET_FRONTMATTER',
|
ON_SET_FRONTMATTER = 'ON_SET_FRONTMATTER',
|
||||||
IMAGE_CLICKED = 'IMAGE_CLICKED',
|
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 {
|
export interface RendererToEditorSimpleMessage {
|
||||||
|
@ -35,8 +36,8 @@ export interface ImageDetails {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetBaseUrlMessage {
|
export interface SetBaseUrlMessage {
|
||||||
type: RenderIframeMessageType.SET_BASE_URL,
|
type: RenderIframeMessageType.SET_BASE_CONFIGURATION,
|
||||||
baseUrl: string
|
baseConfiguration: BaseConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImageClickedMessage {
|
export interface ImageClickedMessage {
|
||||||
|
@ -70,6 +71,11 @@ export interface OnFrontmatterChangeMessage {
|
||||||
frontmatter: NoteFrontmatter | undefined
|
frontmatter: NoteFrontmatter | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OnHeightChangeMessage {
|
||||||
|
type: RenderIframeMessageType.ON_HEIGHT_CHANGE,
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
export type EditorToRendererIframeMessage =
|
export type EditorToRendererIframeMessage =
|
||||||
SetMarkdownContentMessage |
|
SetMarkdownContentMessage |
|
||||||
SetDarkModeMessage |
|
SetDarkModeMessage |
|
||||||
|
@ -82,4 +88,15 @@ export type RendererToEditorIframeMessage =
|
||||||
OnTaskCheckboxChangeMessage |
|
OnTaskCheckboxChangeMessage |
|
||||||
OnFrontmatterChangeMessage |
|
OnFrontmatterChangeMessage |
|
||||||
SetScrollStateMessage |
|
SetScrollStateMessage |
|
||||||
ImageClickedMessage
|
ImageClickedMessage |
|
||||||
|
OnHeightChangeMessage
|
||||||
|
|
||||||
|
export enum RendererType {
|
||||||
|
DOCUMENT,
|
||||||
|
INTRO
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseConfiguration {
|
||||||
|
baseUrl: string
|
||||||
|
rendererType: RendererType
|
||||||
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: darken($dark, 8%);
|
background-color: $dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
@ -87,3 +87,7 @@ body {
|
||||||
.overflow-x-auto {
|
.overflow-x-auto {
|
||||||
overflow-x: auto !important;
|
overflow-x: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overflow-y-hidden {
|
||||||
|
overflow-y: hidden !important;
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
$blue: #337ab7 !default;
|
$blue: #337ab7 !default;
|
||||||
$cyan: #5EB7E0 !default;
|
$cyan: #5EB7E0 !default;
|
||||||
|
$dark: #222222 !default;
|
||||||
|
|
||||||
@import "../../node_modules/bootstrap/scss/functions";
|
@import "../../node_modules/bootstrap/scss/functions";
|
||||||
@import "../../node_modules/bootstrap/scss/mixins";
|
@import "../../node_modules/bootstrap/scss/mixins";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue