Reorganize redux and hooks (1/4) (#985)

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
Tilman Vatteroth 2021-02-01 22:55:49 +01:00 committed by GitHub
parent bdf8110676
commit 1b7abf9f27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 898 additions and 986 deletions

View file

@ -18,7 +18,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
- `{%speakerdeck foobar %}` -> Embedding removed - `{%speakerdeck foobar %}` -> Embedding removed
- `{%pdf https://example.org/example-pdf.pdf %}` -> Embedding removed - `{%pdf https://example.org/example-pdf.pdf %}` -> Embedding removed
- The use of `sequence` as code block language ([Why?](https://hedgedoc.org/faq/)) - The use of `sequence` as code block language ([Why?](https://hedgedoc.org/faq/))
- Comma-separated definition of tags in the yaml-metadata - Comma-separated definition of tags in the yaml-frontmatter
### Removed ### Removed

View file

@ -6,7 +6,7 @@
describe('Autocompletion', () => { describe('Autocompletion', () => {
beforeEach(() => { beforeEach(() => {
cy.visit('/n/test') cy.visitTestEditor()
cy.get('.CodeMirror') cy.get('.CodeMirror')
.click() .click()
.get('textarea') .get('textarea')

View file

@ -9,7 +9,7 @@ import { branding } from '../support/config'
const title = 'This is a test title' const title = 'This is a test title'
describe('Document Title', () => { describe('Document Title', () => {
beforeEach(() => { beforeEach(() => {
cy.visit('/n/test') cy.visitTestEditor()
cy.get('.btn.active.btn-outline-secondary > i.fa-columns') cy.get('.btn.active.btn-outline-secondary > i.fa-columns')
.should('exist') .should('exist')
}) })

View file

@ -6,14 +6,14 @@
describe('Editor mode from URL parameter is used', () => { describe('Editor mode from URL parameter is used', () => {
it('mode view', () => { it('mode view', () => {
cy.visit('/n/features?view') cy.visitTestEditor('view')
cy.get('.splitter.left') cy.get('.splitter.left')
.should('have.class', 'd-none') .should('have.class', 'd-none')
cy.get('.splitter.right') cy.get('.splitter.right')
.should('not.have.class', 'd-none') .should('not.have.class', 'd-none')
}) })
it('mode both', () => { it('mode both', () => {
cy.visit('/n/features?both') cy.visitTestEditor('both')
cy.get('.splitter.left') cy.get('.splitter.left')
.should('not.have.class', 'd-none') .should('not.have.class', 'd-none')
cy.get('.splitter.separator') cy.get('.splitter.separator')
@ -22,7 +22,7 @@ describe('Editor mode from URL parameter is used', () => {
.should('not.have.class', 'd-none') .should('not.have.class', 'd-none')
}) })
it('mode edit', () => { it('mode edit', () => {
cy.visit('/n/features?edit') cy.visitTestEditor('edit')
cy.get('.splitter.left') cy.get('.splitter.left')
.should('not.have.class', 'd-none') .should('not.have.class', 'd-none')
cy.get('.splitter.right') cy.get('.splitter.right')

View file

@ -9,7 +9,7 @@ describe('Export', () => {
const testContent = `---\ntitle: ${testTitle}\n---\nThis is some test content` const testContent = `---\ntitle: ${testTitle}\n---\nThis is some test content`
beforeEach(() => { beforeEach(() => {
cy.visit('/n/test') cy.visitTestEditor()
cy.codemirrorFill(testContent) cy.codemirrorFill(testContent)
}) })

View file

@ -8,7 +8,7 @@ const imageUrl = 'http://example.com/non-existing.png'
describe('File upload', () => { describe('File upload', () => {
beforeEach(() => { beforeEach(() => {
cy.visit('/n/test') cy.visitTestEditor()
}) })
it('doesn\'t prevent drag\'n\'drop of plain text', () => { it('doesn\'t prevent drag\'n\'drop of plain text', () => {

View file

@ -6,7 +6,7 @@
describe('Help Dialog', () => { describe('Help Dialog', () => {
beforeEach(() => { beforeEach(() => {
cy.visit('/n/test') cy.visitTestEditor()
}) })
it('ToDo-List', () => { it('ToDo-List', () => {

View file

@ -12,7 +12,7 @@ const findHljsCodeBlock = () => {
describe('Code', () => { describe('Code', () => {
beforeEach(() => { beforeEach(() => {
cy.visit('/n/test') cy.visitTestEditor()
}) })
describe('with just the language', () => { describe('with just the language', () => {

View file

@ -6,7 +6,7 @@
describe('Import markdown file', () => { describe('Import markdown file', () => {
beforeEach(() => { beforeEach(() => {
cy.visit('/n/test') cy.visitTestEditor()
}) })
it('import on blank note', () => { it('import on blank note', () => {

View file

@ -10,7 +10,7 @@ describe('The status bar text length info', () => {
const tooMuchTestContent = `${dangerTestContent}a` const tooMuchTestContent = `${dangerTestContent}a`
beforeEach(() => { beforeEach(() => {
cy.visit('/n/test') cy.visitTestEditor()
}) })
it('shows the maximal length of the document as number of available characters in the tooltip', () => { it('shows the maximal length of the document as number of available characters in the tooltip', () => {

View file

@ -9,7 +9,7 @@ describe('Toolbar Buttons', () => {
const testLink = 'http://hedgedoc.org' const testLink = 'http://hedgedoc.org'
beforeEach(() => { beforeEach(() => {
cy.visit('/n/test') cy.visitTestEditor()
cy.get('.CodeMirror') cy.get('.CodeMirror')
.click() .click()

View file

@ -6,7 +6,7 @@
describe('YAML Array for deprecated syntax of document tags in frontmatter', () => { describe('YAML Array for deprecated syntax of document tags in frontmatter', () => {
beforeEach(() => { beforeEach(() => {
cy.visit('/n/features') cy.visitTestEditor()
}) })
it('is shown when using old syntax', () => { it('is shown when using old syntax', () => {

View file

@ -26,3 +26,4 @@ import './config'
import './fill' import './fill'
import './getMarkdownRenderer' import './getMarkdownRenderer'
import './login' import './login'
import './visit-test-editor'

View file

@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare namespace Cypress {
interface Chainable {
visitTestEditor (query?: string): Chainable<Cypress.AUTWindow>
}
}
export const testNoteId = 'test'
Cypress.Commands.add('visitTestEditor', (query?: string) => {
return cy.visit(`/n/${testNoteId}${query ? `?${query}` : ''}`)
})
beforeEach(() => {
cy.intercept(`/api/v2/notes/${testNoteId}-get`, {
"id": "ABC123",
"alias": "banner",
"lastChange": {
"userId": "test",
"timestamp": 1600033920
},
"viewCount": 0,
"createTime": 1600033920,
"content": "",
"authorship": [],
"preVersionTwoNote": true
})
})

File diff suppressed because one or more lines are too long

View file

@ -15,15 +15,17 @@ export interface Note {
id: string id: string
alias: string alias: string
lastChange: LastChange lastChange: LastChange
viewcount: number viewCount: number
createtime: number createTime: number
content: string content: string
authorship: number[] authorship: number[]
preVersionTwoNote: boolean preVersionTwoNote: boolean
} }
export const getNote = async (noteId: string): Promise<Note> => { export const getNote = async (noteId: string): Promise<Note> => {
const response = await fetch(getApiUrl() + `/notes/${noteId}`, { // The "-get" suffix is necessary, because in our mock api (filesystem) the note id might already be a folder.
// TODO: [mrdrogdrog] replace -get with actual api route as soon as api backend is ready.
const response = await fetch(getApiUrl() + `/notes/${noteId}-get`, {
...defaultFetchConfig ...defaultFetchConfig
}) })
expectResponseCode(response) expectResponseCode(response)

View file

@ -1,17 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { YAMLMetaData } from '../../editor/yaml-metadata/yaml-metadata'
export const extractNoteTitle = (defaultTitle: string, noteMetadata?: YAMLMetaData, firstHeading?: string): string => {
if (noteMetadata?.title && noteMetadata?.title !== '') {
return noteMetadata.title
} else if (noteMetadata?.opengraph && noteMetadata?.opengraph.get('title') && noteMetadata?.opengraph.get('title') !== '') {
return (noteMetadata?.opengraph.get('title') ?? defaultTitle)
} else {
return (firstHeading ?? defaultTitle).trim()
}
}

View file

@ -1,20 +1,19 @@
/* /*
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, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { Alert, Button, Col, ListGroup, Modal, Row } from 'react-bootstrap' import { Alert, Button, Col, ListGroup, Modal, Row } from 'react-bootstrap'
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer' import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useParams } from 'react-router' import { useParams } from 'react-router'
import { getAllRevisions, getRevision } from '../../../../api/revisions' import { getAllRevisions, getRevision } from '../../../../api/revisions'
import { Revision, RevisionListEntry } from '../../../../api/revisions/types' import { Revision, RevisionListEntry } from '../../../../api/revisions/types'
import { UserResponse } from '../../../../api/users/types' import { UserResponse } from '../../../../api/users/types'
import { useIsDarkModeActivated } from '../../../../hooks/common/use-is-dark-mode-activated' import { useIsDarkModeActivated } from '../../../../hooks/common/use-is-dark-mode-activated'
import { ApplicationState } from '../../../../redux' import { useNoteMarkdownContent } from '../../../../hooks/common/use-note-markdown-content'
import { CommonModal } from '../../../common/modals/common-modal' import { CommonModal } from '../../../common/modals/common-modal'
import { ShowIf } from '../../../common/show-if/show-if' import { ShowIf } from '../../../common/show-if/show-if'
import { RevisionModalListEntry } from './revision-modal-list-entry' import { RevisionModalListEntry } from './revision-modal-list-entry'
@ -58,7 +57,7 @@ export const RevisionModal: React.FC<PermissionsModalProps> = ({ show, onHide })
}).catch(() => setError(true)) }).catch(() => setError(true))
}, [selectedRevisionTimestamp, id]) }, [selectedRevisionTimestamp, id])
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content) const markdownContent = useNoteMarkdownContent()
return ( return (
<CommonModal show={show} onHide={onHide} titleI18nKey={'editor.modal.revision.title'} icon={'history'} closeButton={true} size={'xl'} additionalClasses='revision-modal'> <CommonModal show={show} onHide={onHide} titleI18nKey={'editor.modal.revision.title'} icon={'history'} closeButton={true} size={'xl'} additionalClasses='revision-modal'>

View file

@ -7,7 +7,7 @@
import equal from 'fast-deep-equal' import equal from 'fast-deep-equal'
import React from 'react' import React from 'react'
import { Modal } from 'react-bootstrap' import { Modal } from 'react-bootstrap'
import { useTranslation , Trans } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url' import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url'
@ -24,7 +24,7 @@ export interface ShareModalProps {
export const ShareModal: React.FC<ShareModalProps> = ({ show, onHide }) => { export const ShareModal: React.FC<ShareModalProps> = ({ show, onHide }) => {
useTranslation() useTranslation()
const noteMetadata = useSelector((state: ApplicationState) => state.documentContent.metadata, equal) const noteFrontmatter = useSelector((state: ApplicationState) => state.noteDetails.frontmatter, equal)
const editorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode) const editorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
const baseUrl = useFrontendBaseUrl() const baseUrl = useFrontendBaseUrl()
const { id } = useParams<EditorPathParams>() const { id } = useParams<EditorPathParams>()
@ -39,11 +39,11 @@ export const ShareModal: React.FC<ShareModalProps> = ({ show, onHide }) => {
<Trans i18nKey={'editor.modal.shareLink.editorDescription'}/> <Trans i18nKey={'editor.modal.shareLink.editorDescription'}/>
<CopyableField content={`${baseUrl}/n/${id}?${editorMode}`} nativeShareButton={true} <CopyableField content={`${baseUrl}/n/${id}?${editorMode}`} nativeShareButton={true}
url={`${baseUrl}/n/${id}?${editorMode}`}/> url={`${baseUrl}/n/${id}?${editorMode}`}/>
<ShowIf condition={noteMetadata.type === 'slide'}> <ShowIf condition={noteFrontmatter.type === 'slide'}>
<Trans i18nKey={'editor.modal.shareLink.slidesDescription'}/> <Trans i18nKey={'editor.modal.shareLink.slidesDescription'}/>
<CopyableField content={`${baseUrl}/p/${id}`} nativeShareButton={true} url={`${baseUrl}/p/${id}`}/> <CopyableField content={`${baseUrl}/p/${id}`} nativeShareButton={true} url={`${baseUrl}/p/${id}`}/>
</ShowIf> </ShowIf>
<ShowIf condition={noteMetadata.type === ''}> <ShowIf condition={noteFrontmatter.type === ''}>
<Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'}/> <Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'}/>
<CopyableField content={`${baseUrl}/s/${id}`} nativeShareButton={true} url={`${baseUrl}/s/${id}`}/> <CopyableField content={`${baseUrl}/s/${id}`} nativeShareButton={true} url={`${baseUrl}/s/${id}`}/>
</ShowIf> </ShowIf>

View file

@ -3,21 +3,24 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import equal from 'fast-deep-equal'
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react' import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated' import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated'
import { ApplicationState } from '../../../redux' import { ApplicationState } from '../../../redux'
import { isTestMode } from '../../../utils/is-test-mode' import { isTestMode } from '../../../utils/is-test-mode'
import { ImageLightboxModal } from '../../markdown-renderer/replace-components/image/image-lightbox-modal'
import { IframeEditorToRendererCommunicator } from '../../render-page/iframe-editor-to-renderer-communicator' import { IframeEditorToRendererCommunicator } from '../../render-page/iframe-editor-to-renderer-communicator'
import { ImageDetails } from '../../render-page/rendering-message' import { ImageDetails } from '../../render-page/rendering-message'
import { ScrollingDocumentRenderPaneProps } from './scrolling-document-render-pane' import { ScrollState } from '../scroll/scroll-props'
import { DocumentRenderPaneProps } from './document-render-pane'
import { useOnIframeLoad } from './hooks/use-on-iframe-load'
import { ShowOnPropChangeImageLightbox } from './show-on-prop-change-image-lightbox'
export const DocumentIframe: React.FC<ScrollingDocumentRenderPaneProps> = ( export const DocumentIframe: React.FC<DocumentRenderPaneProps> = (
{ {
markdownContent, markdownContent,
onTaskCheckedChange, onTaskCheckedChange,
onMetadataChange, onFrontmatterChange,
scrollState, scrollState,
onFirstHeadingChange, onFirstHeadingChange,
wide, wide,
@ -25,40 +28,40 @@ export const DocumentIframe: React.FC<ScrollingDocumentRenderPaneProps> = (
onMakeScrollSource, onMakeScrollSource,
extraClasses extraClasses
}) => { }) => {
const frameReference = useRef<HTMLIFrameElement>(null)
const darkMode = useIsDarkModeActivated() const darkMode = useIsDarkModeActivated()
const [rendererReady, setRendererReady] = useState<boolean>(false)
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined) const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
const frameReference = useRef<HTMLIFrameElement>(null)
const rendererOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.rendererOrigin) const rendererOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.rendererOrigin)
const renderPageUrl = `${rendererOrigin}/render` const renderPageUrl = `${rendererOrigin}/render`
const resetRendererReady = useCallback(() => setRendererReady(false), [])
const iframeCommunicator = useMemo(() => new IframeEditorToRendererCommunicator(), []) const iframeCommunicator = useMemo(() => new IframeEditorToRendererCommunicator(), [])
const onIframeLoad = useOnIframeLoad(frameReference, iframeCommunicator, rendererOrigin, renderPageUrl, resetRendererReady)
useEffect(() => () => iframeCommunicator.unregisterEventListener(), [iframeCommunicator]) useEffect(() => () => iframeCommunicator.unregisterEventListener(), [iframeCommunicator])
const [rendererReady, setRendererReady] = useState<boolean>(false)
useEffect(() => iframeCommunicator.onFirstHeadingChange(onFirstHeadingChange), [iframeCommunicator, onFirstHeadingChange]) useEffect(() => iframeCommunicator.onFirstHeadingChange(onFirstHeadingChange), [iframeCommunicator, onFirstHeadingChange])
useEffect(() => iframeCommunicator.onMetaDataChange(onMetadataChange), [iframeCommunicator, onMetadataChange]) useEffect(() => iframeCommunicator.onFrontmatterChange(onFrontmatterChange), [iframeCommunicator, onFrontmatterChange])
useEffect(() => iframeCommunicator.onSetScrollState(onScroll), [iframeCommunicator, onScroll]) useEffect(() => iframeCommunicator.onSetScrollState(onScroll), [iframeCommunicator, onScroll])
useEffect(() => iframeCommunicator.onSetScrollSourceToRenderer(onMakeScrollSource), [iframeCommunicator, onMakeScrollSource]) useEffect(() => iframeCommunicator.onSetScrollSourceToRenderer(onMakeScrollSource), [iframeCommunicator, onMakeScrollSource])
useEffect(() => iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange), [iframeCommunicator, onTaskCheckedChange]) useEffect(() => iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange), [iframeCommunicator, onTaskCheckedChange])
useEffect(() => iframeCommunicator.onImageClicked(setLightboxDetails), [iframeCommunicator]) useEffect(() => iframeCommunicator.onImageClicked(setLightboxDetails), [iframeCommunicator])
useEffect(() => iframeCommunicator.onRendererReady(() => setRendererReady(true)), [darkMode, iframeCommunicator, scrollState, wide]) useEffect(() => iframeCommunicator.onRendererReady(() => setRendererReady(true)), [darkMode, iframeCommunicator, scrollState, wide])
useEffect(() => {
if (rendererReady) {
iframeCommunicator.sendSetMarkdownContent(markdownContent)
}
}, [iframeCommunicator, markdownContent, rendererReady])
useEffect(() => { useEffect(() => {
if (rendererReady) { if (rendererReady) {
iframeCommunicator.sendSetDarkmode(darkMode) iframeCommunicator.sendSetDarkmode(darkMode)
} }
}, [darkMode, iframeCommunicator, rendererReady]) }, [darkMode, iframeCommunicator, rendererReady])
const oldScrollState = useRef<ScrollState | undefined>(undefined)
useEffect(() => { useEffect(() => {
if (rendererReady) { if (rendererReady && !equal(scrollState, oldScrollState.current)) {
oldScrollState.current = scrollState
iframeCommunicator.sendScrollState(scrollState) iframeCommunicator.sendScrollState(scrollState)
} }
}, [iframeCommunicator, rendererReady, scrollState]) }, [iframeCommunicator, rendererReady, scrollState])
useEffect(() => { useEffect(() => {
if (rendererReady) { if (rendererReady) {
iframeCommunicator.sendSetWide(wide ?? false) iframeCommunicator.sendSetWide(wide ?? false)
@ -69,37 +72,17 @@ export const DocumentIframe: React.FC<ScrollingDocumentRenderPaneProps> = (
if (rendererReady) { if (rendererReady) {
iframeCommunicator.sendSetBaseUrl(window.location.toString()) iframeCommunicator.sendSetBaseUrl(window.location.toString())
} }
}, [iframeCommunicator, rendererReady,]) }, [iframeCommunicator, rendererReady])
const sendToRenderPage = useRef<boolean>(true) useEffect(() => {
if (rendererReady) {
const onLoad = useCallback(() => { iframeCommunicator.sendSetMarkdownContent(markdownContent)
const frame = frameReference.current
if (!frame || !frame.contentWindow) {
iframeCommunicator.unsetOtherSide()
return
} }
}, [iframeCommunicator, markdownContent, rendererReady])
if (sendToRenderPage.current) {
iframeCommunicator.setOtherSide(frame.contentWindow, rendererOrigin)
sendToRenderPage.current = false
return
} else {
setRendererReady(false)
console.error("Navigated away from unknown URL")
frame.src = renderPageUrl
sendToRenderPage.current = true
}
}, [iframeCommunicator, renderPageUrl, rendererOrigin])
const hideLightbox = useCallback(() => {
setLightboxDetails(undefined)
}, [])
return <Fragment> return <Fragment>
<ImageLightboxModal show={!!lightboxDetails} onHide={hideLightbox} src={lightboxDetails?.src} <ShowOnPropChangeImageLightbox details={lightboxDetails}/>
alt={lightboxDetails?.alt} title={lightboxDetails?.title}/> <iframe data-cy={'documentIframe'} onLoad={onIframeLoad} title="render" src={renderPageUrl}
<iframe data-cy={'documentIframe'} onLoad={onLoad} 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={`h-100 w-100 border-0 ${extraClasses ?? ''}`}/>
</Fragment> </Fragment>

View file

@ -5,26 +5,23 @@
*/ */
import { TocAst } from 'markdown-it-toc-done-right' import { TocAst } from 'markdown-it-toc-done-right'
import React, { MutableRefObject, useCallback, useRef, useState } from 'react' import React, { MutableRefObject, 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'
import { ShowIf } from '../../common/show-if/show-if' import { ShowIf } from '../../common/show-if/show-if'
import { FullMarkdownRenderer } from '../../markdown-renderer/full-markdown-renderer' import { FullMarkdownRenderer } from '../../markdown-renderer/full-markdown-renderer'
import { ImageClickHandler } from '../../markdown-renderer/replace-components/image/image-replacer' import { ImageClickHandler } from '../../markdown-renderer/replace-components/image/image-replacer'
import { LineMarkerPosition } from '../../markdown-renderer/types' import { NoteFrontmatter } from '../note-frontmatter/note-frontmatter'
import { ScrollProps } from '../scroll/scroll-props'
import { TableOfContents } from '../table-of-contents/table-of-contents' import { TableOfContents } from '../table-of-contents/table-of-contents'
import { YAMLMetaData } from '../yaml-metadata/yaml-metadata' import { useSyncedScrolling } from './hooks/use-synced-scrolling'
import { useAdaptedLineMarkerCallback } from './use-adapted-line-markers-callback'
import { YamlArrayDeprecationAlert } from './yaml-array-deprecation-alert' import { YamlArrayDeprecationAlert } from './yaml-array-deprecation-alert'
export interface DocumentRenderPaneProps { export interface DocumentRenderPaneProps extends ScrollProps {
extraClasses?: string extraClasses?: string
onFirstHeadingChange?: (firstHeading: string | undefined) => void onFirstHeadingChange?: (firstHeading: string | undefined) => void
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
onMetadataChange?: (metaData: YAMLMetaData | undefined) => void
onMouseEnterRenderer?: () => void
onScrollRenderer?: () => void
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null> documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
wide?: boolean, wide?: boolean,
@ -37,33 +34,27 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
{ {
extraClasses, extraClasses,
onFirstHeadingChange, onFirstHeadingChange,
onLineMarkerPositionChanged, onFrontmatterChange,
onMetadataChange, onMakeScrollSource,
onMouseEnterRenderer,
onScrollRenderer,
onTaskCheckedChange, onTaskCheckedChange,
documentRenderPaneRef,
wide, wide,
baseUrl, baseUrl,
markdownContent, markdownContent,
onImageClick onImageClick,
onScroll,
scrollState
}) => { }) => {
const [tocAst, setTocAst] = useState<TocAst>()
const internalDocumentRenderPaneRef = useRef<HTMLDivElement>()
const { width } = useResizeObserver({ ref: internalDocumentRenderPaneRef.current })
const realWidth = width ?? 0
const rendererRef = useRef<HTMLDivElement | null>(null) const rendererRef = useRef<HTMLDivElement | null>(null)
const changeLineMarker = useAdaptedLineMarkerCallback(documentRenderPaneRef, rendererRef, onLineMarkerPositionChanged) const internalDocumentRenderPaneRef = useRef<HTMLDivElement>(null)
const setContainerReference = useCallback((instance: HTMLDivElement | null) => { const [tocAst, setTocAst] = useState<TocAst>()
if (documentRenderPaneRef) { const width = useResizeObserver({ ref: internalDocumentRenderPaneRef.current }).width ?? 0
documentRenderPaneRef.current = instance || null
} const contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent])
internalDocumentRenderPaneRef.current = instance || undefined const [onLineMarkerPositionChanged, onUserScroll] = useSyncedScrolling(internalDocumentRenderPaneRef, rendererRef, contentLineCount, scrollState, onScroll)
}, [documentRenderPaneRef])
return ( return (
<div className={`bg-light m-0 pb-5 row ${extraClasses ?? ''}`} <div className={`overflow-y-scroll h-100 bg-light m-0 pb-5 row ${extraClasses ?? ''}`}
ref={setContainerReference} onScroll={onScrollRenderer} onMouseEnter={onMouseEnterRenderer}> ref={internalDocumentRenderPaneRef} onScroll={onUserScroll} onMouseEnter={onMakeScrollSource}>
<div className={'col-md d-none d-md-block'}/> <div className={'col-md d-none d-md-block'}/>
<div className={'bg-light col'}> <div className={'bg-light col'}>
<YamlArrayDeprecationAlert/> <YamlArrayDeprecationAlert/>
@ -73,8 +64,8 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
className={'flex-fill pt-4 mb-3'} className={'flex-fill pt-4 mb-3'}
content={markdownContent} content={markdownContent}
onFirstHeadingChange={onFirstHeadingChange} onFirstHeadingChange={onFirstHeadingChange}
onLineMarkerPositionChanged={changeLineMarker} onLineMarkerPositionChanged={onLineMarkerPositionChanged}
onMetaDataChange={onMetadataChange} onFrontmatterChange={onFrontmatterChange}
onTaskCheckedChange={onTaskCheckedChange} onTaskCheckedChange={onTaskCheckedChange}
onTocChange={(tocAst) => setTocAst(tocAst)} onTocChange={(tocAst) => setTocAst(tocAst)}
wide={wide} wide={wide}
@ -85,10 +76,10 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
<div className={'col-md pt-4'}> <div className={'col-md pt-4'}>
<ShowIf condition={!!tocAst}> <ShowIf condition={!!tocAst}>
<ShowIf condition={realWidth >= 1280}> <ShowIf condition={width >= 1280}>
<TableOfContents ast={tocAst as TocAst} className={'sticky'} baseUrl={baseUrl}/> <TableOfContents ast={tocAst as TocAst} className={'sticky'} baseUrl={baseUrl}/>
</ShowIf> </ShowIf>
<ShowIf condition={realWidth < 1280}> <ShowIf condition={width < 1280}>
<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'}>

View file

@ -1,11 +1,11 @@
/* /*
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 { RefObject, useCallback } from 'react' import { RefObject, useCallback } from 'react'
import { LineMarkerPosition } from '../../markdown-renderer/types' import { LineMarkerPosition } from '../../../markdown-renderer/types'
export const useAdaptedLineMarkerCallback = (documentRenderPaneRef: RefObject<HTMLDivElement> | undefined, export const useAdaptedLineMarkerCallback = (documentRenderPaneRef: RefObject<HTMLDivElement> | undefined,
rendererRef: RefObject<HTMLDivElement>, rendererRef: RefObject<HTMLDivElement>,

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { RefObject, useCallback, useRef } from 'react'
import { IframeEditorToRendererCommunicator } from '../../../render-page/iframe-editor-to-renderer-communicator'
export const useOnIframeLoad = (frameReference: RefObject<HTMLIFrameElement>, iframeCommunicator: IframeEditorToRendererCommunicator,
rendererOrigin: string, renderPageUrl: string, onNavigateAway: () => void): () => void => {
const sendToRenderPage = useRef<boolean>(true)
return useCallback(() => {
const frame = frameReference.current
if (!frame || !frame.contentWindow) {
iframeCommunicator.unsetOtherSide()
return
}
if (sendToRenderPage.current) {
iframeCommunicator.setOtherSide(frame.contentWindow, rendererOrigin)
sendToRenderPage.current = false
return
} else {
onNavigateAway()
console.error("Navigated away from unknown URL")
frame.src = renderPageUrl
sendToRenderPage.current = true
}
}, [frameReference, iframeCommunicator, onNavigateAway, renderPageUrl, rendererOrigin])
}

View file

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useState } from 'react'
import { LineMarkerPosition } from '../../../markdown-renderer/types'
import { useOnUserScroll } from '../../scroll/hooks/use-on-user-scroll'
import { useScrollToLineMark } from '../../scroll/hooks/use-scroll-to-line-mark'
import { ScrollState } from '../../scroll/scroll-props'
export const useSyncedScrolling = (outerContainerRef: React.RefObject<HTMLElement>,
rendererRef: React.RefObject<HTMLElement>,
numberOfLines: number,
scrollState?: ScrollState,
onScroll?: (scrollState: ScrollState) => void): [(lineMarkers: LineMarkerPosition[]) => void, () => void] => {
const [lineMarks, setLineMarks] = useState<LineMarkerPosition[]>()
const onLineMarkerPositionChanged = useCallback((linkMarkerPositions: LineMarkerPosition[]) => {
if (!outerContainerRef.current || !rendererRef.current) {
return
}
const documentRenderPaneTop = (outerContainerRef.current.offsetTop ?? 0)
const rendererTop = (rendererRef.current.offsetTop ?? 0)
const offset = rendererTop - documentRenderPaneTop
const adjustedLineMakerPositions = linkMarkerPositions.map(oldMarker => ({
line: oldMarker.line,
position: oldMarker.position + offset
}))
setLineMarks(adjustedLineMakerPositions)
}, [outerContainerRef, rendererRef])
const onUserScroll = useOnUserScroll(lineMarks, outerContainerRef, onScroll)
useScrollToLineMark(scrollState, lineMarks, numberOfLines, outerContainerRef)
return [onLineMarkerPositionChanged, onUserScroll]
}

View file

@ -1,59 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useMemo, useRef, useState } from 'react'
import { LineMarkerPosition } from '../../markdown-renderer/types'
import { useOnUserScroll } from '../scroll/hooks/use-on-user-scroll'
import { useScrollToLineMark } from '../scroll/hooks/use-scroll-to-line-mark'
import { ScrollProps } from '../scroll/scroll-props'
import { DocumentRenderPane, DocumentRenderPaneProps } from './document-render-pane'
type ImplementedProps =
'onLineMarkerPositionChanged'
| 'onScrollRenderer'
| 'rendererReference'
| 'onMouseEnterRenderer'
export type ScrollingDocumentRenderPaneProps = Omit<(DocumentRenderPaneProps & ScrollProps), ImplementedProps>
export const ScrollingDocumentRenderPane: React.FC<ScrollingDocumentRenderPaneProps> = (
{
scrollState,
wide,
onFirstHeadingChange,
onMakeScrollSource,
onMetadataChange,
onScroll,
onTaskCheckedChange,
markdownContent,
extraClasses,
baseUrl,
onImageClick
}) => {
const renderer = useRef<HTMLDivElement>(null)
const [lineMarks, setLineMarks] = useState<LineMarkerPosition[]>()
const contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent])
useScrollToLineMark(scrollState, lineMarks, contentLineCount, renderer)
const userScroll = useOnUserScroll(lineMarks, renderer, onScroll)
return (
<DocumentRenderPane
extraClasses={`overflow-y-scroll h-100 ${extraClasses || ''}`}
documentRenderPaneRef={renderer}
wide={wide}
onFirstHeadingChange={onFirstHeadingChange}
onLineMarkerPositionChanged={setLineMarks}
onMetadataChange={onMetadataChange}
onMouseEnterRenderer={onMakeScrollSource}
onScrollRenderer={userScroll}
onTaskCheckedChange={onTaskCheckedChange}
markdownContent={markdownContent}
baseUrl={baseUrl}
onImageClick={onImageClick}
/>
)
}

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useEffect, useState } from 'react'
import { ImageLightboxModal } from '../../markdown-renderer/replace-components/image/image-lightbox-modal'
import { ImageDetails } from '../../render-page/rendering-message'
export interface ShowOnPropChangeImageLightboxProps {
details?: ImageDetails
}
export const ShowOnPropChangeImageLightbox: React.FC<ShowOnPropChangeImageLightboxProps> = ({ details }) => {
const [show, setShow] = useState<boolean>(false)
const hideLightbox = useCallback(() => {
setShow(false)
}, [])
useEffect(() => {
if (details) {
setShow(true)
}
}, [details])
return (
<ImageLightboxModal show={show} onHide={hideLightbox} src={details?.src}
alt={details?.alt} title={details?.title}/>
)
}

View file

@ -15,7 +15,7 @@ import { ShowIf } from '../../common/show-if/show-if'
export const YamlArrayDeprecationAlert: React.FC = () => { export const YamlArrayDeprecationAlert: React.FC = () => {
useTranslation() useTranslation()
const yamlDeprecatedTags = useSelector((state: ApplicationState) => state.documentContent.metadata.deprecatedTagsSyntax) const yamlDeprecatedTags = useSelector((state: ApplicationState) => state.noteDetails.frontmatter.deprecatedTagsSyntax)
return <ShowIf condition={yamlDeprecatedTags}> return <ShowIf condition={yamlDeprecatedTags}>
<Alert data-cy={'yamlArrayDeprecationAlert'} className={'text-wrap'} variant='warning' dir='auto'> <Alert data-cy={'yamlArrayDeprecationAlert'} className={'text-wrap'} variant='warning' dir='auto'>

View file

@ -21,7 +21,7 @@ export const handleUpload = (file: File, editor: Editor): void => {
} }
const cursor = editor.getCursor() const cursor = editor.getCursor()
const uploadPlaceholder = `![${i18n.t('editor.upload.uploadFile', { fileName: file.name })}]()` const uploadPlaceholder = `![${i18n.t('editor.upload.uploadFile', { fileName: file.name })}]()`
const noteId = store.getState().documentContent.noteId const noteId = store.getState().noteDetails.id
editor.replaceRange(uploadPlaceholder, cursor, cursor, '+input') editor.replaceRange(uploadPlaceholder, cursor, cursor, '+input')
uploadFile(noteId, mimeType, file) uploadFile(noteId, mimeType, file)
.then(({ link }) => { .then(({ link }) => {

View file

@ -7,25 +7,32 @@
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react' import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { useLocation, useParams } from 'react-router' import { useLocation } from 'react-router'
import useMedia from 'use-media' import useMedia from 'use-media'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode' import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { useDocumentTitle } from '../../hooks/common/use-document-title' import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
import { ApplicationState } from '../../redux' import { ApplicationState } from '../../redux'
import { setDocumentContent, setDocumentMetadata, setNoteId } from '../../redux/document-content/methods'
import { setEditorMode } from '../../redux/editor/methods' import { setEditorMode } from '../../redux/editor/methods'
import { extractNoteTitle } from '../common/document-title/note-title-extractor' import {
SetCheckboxInMarkdownContent,
setNoteFrontmatter,
setNoteMarkdownContent,
updateNoteTitleByFirstHeading
} from '../../redux/note-details/methods'
import { MotdBanner } from '../common/motd-banner/motd-banner' import { MotdBanner } from '../common/motd-banner/motd-banner'
import { ShowIf } from '../common/show-if/show-if'
import { ErrorWhileLoadingNoteAlert } from '../pad-view-only/ErrorWhileLoadingNoteAlert'
import { LoadingNoteAlert } from '../pad-view-only/LoadingNoteAlert'
import { AppBar, AppBarMode } from './app-bar/app-bar' import { AppBar, AppBarMode } from './app-bar/app-bar'
import { EditorMode } from './app-bar/editor-view-mode' import { EditorMode } from './app-bar/editor-view-mode'
import { DocumentIframe } from './document-renderer-pane/document-iframe' import { DocumentIframe } from './document-renderer-pane/document-iframe'
import { EditorPane } from './editor-pane/editor-pane' import { EditorPane } from './editor-pane/editor-pane'
import { editorTestContent } from './editorTestContent'
import { useViewModeShortcuts } from './hooks/useViewModeShortcuts' import { useViewModeShortcuts } from './hooks/useViewModeShortcuts'
import { DualScrollState, ScrollState } from './scroll/scroll-props' import { DualScrollState, ScrollState } from './scroll/scroll-props'
import { Sidebar } from './sidebar/sidebar' import { Sidebar } from './sidebar/sidebar'
import { Splitter } from './splitter/splitter' import { Splitter } from './splitter/splitter'
import { YAMLMetaData } from './yaml-metadata/yaml-metadata' import { useLoadNoteFromServer } from './useLoadNoteFromServer'
export interface EditorPathParams { export interface EditorPathParams {
id: string id: string
@ -36,18 +43,11 @@ export enum ScrollSource {
RENDERER RENDERER
} }
const TASK_REGEX = /(\s*[-*] )(\[[ xX]])( .*)/
export const Editor: React.FC = () => { export const Editor: React.FC = () => {
const { t } = useTranslation() useTranslation()
const { id } = useParams<EditorPathParams>()
const { search } = useLocation() const { search } = useLocation()
const untitledNote = t('editor.untitledNote') const markdownContent = useNoteMarkdownContent()
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
const isWide = useMedia({ minWidth: 576 }, true) const isWide = useMedia({ minWidth: 576 }, true)
const [documentTitle, setDocumentTitle] = useState(untitledNote)
const noteMetadata = useRef<YAMLMetaData>()
const firstHeading = useRef<string>()
const scrollSource = useRef<ScrollSource>(ScrollSource.EDITOR) const scrollSource = useRef<ScrollSource>(ScrollSource.EDITOR)
const editorMode: EditorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode) const editorMode: EditorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
@ -59,7 +59,6 @@ export const Editor: React.FC = () => {
})) }))
useEffect(() => { useEffect(() => {
setDocumentContent(editorTestContent)
const requestedMode = search.substr(1) const requestedMode = search.substr(1)
const mode = Object.values(EditorMode).find(mode => mode === requestedMode) const mode = Object.values(EditorMode).find(mode => mode === requestedMode)
if (mode) { if (mode) {
@ -67,39 +66,6 @@ export const Editor: React.FC = () => {
} }
}, [search]) }, [search])
const updateDocumentTitle = useCallback(() => {
const noteTitle = extractNoteTitle(untitledNote, noteMetadata.current, firstHeading.current)
setDocumentTitle(noteTitle)
}, [noteMetadata, firstHeading, untitledNote])
const onFirstHeadingChange = useCallback((newFirstHeading: string | undefined) => {
firstHeading.current = newFirstHeading
updateDocumentTitle()
}, [updateDocumentTitle])
const onMetadataChange = useCallback((metaData: YAMLMetaData | undefined) => {
noteMetadata.current = metaData
setDocumentMetadata(metaData)
updateDocumentTitle()
}, [updateDocumentTitle])
const onTaskCheckedChange = useCallback((lineInMarkdown: number, checked: boolean) => {
const lines = markdownContent.split('\n')
const results = TASK_REGEX.exec(lines[lineInMarkdown])
if (results) {
const before = results[1]
const after = results[3]
lines[lineInMarkdown] = `${before}[${checked ? 'x' : ' '}]${after}`
setDocumentContent(lines.join('\n'))
}
}, [markdownContent])
useViewModeShortcuts()
useEffect(() => {
setNoteId(id)
}, [id])
useEffect(() => { useEffect(() => {
if (!isWide && editorMode === EditorMode.BOTH) { if (!isWide && editorMode === EditorMode.BOTH) {
setEditorMode(EditorMode.PREVIEW) setEditorMode(EditorMode.PREVIEW)
@ -118,8 +84,10 @@ export const Editor: React.FC = () => {
} }
}, [editorSyncScroll]) }, [editorSyncScroll])
useViewModeShortcuts()
useApplyDarkMode() useApplyDarkMode()
useDocumentTitle(documentTitle) useDocumentTitleWithNoteTitle()
const [error, loading] = useLoadNoteFromServer()
const setRendererToScrollSource = useCallback(() => { const setRendererToScrollSource = useCallback(() => {
scrollSource.current = ScrollSource.RENDERER scrollSource.current = ScrollSource.RENDERER
@ -134,32 +102,39 @@ export const Editor: React.FC = () => {
<MotdBanner/> <MotdBanner/>
<div className={'d-flex flex-column vh-100'}> <div className={'d-flex flex-column vh-100'}>
<AppBar mode={AppBarMode.EDITOR}/> <AppBar mode={AppBarMode.EDITOR}/>
<div className={"flex-fill d-flex h-100 w-100 overflow-hidden flex-row"}>
<Splitter <div className={'container'}>
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH} <ErrorWhileLoadingNoteAlert show={error}/>
left={ <LoadingNoteAlert show={loading}/>
<EditorPane
onContentChange={setDocumentContent}
content={markdownContent}
scrollState={scrollState.editorScrollState}
onScroll={onEditorScroll}
onMakeScrollSource={setEditorToScrollSource}/>
}
showRight={editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH}
right={
<DocumentIframe
markdownContent={markdownContent}
onMakeScrollSource={setRendererToScrollSource}
onFirstHeadingChange={onFirstHeadingChange}
onTaskCheckedChange={onTaskCheckedChange}
onMetadataChange={onMetadataChange}
onScroll={onMarkdownRendererScroll}
wide={editorMode === EditorMode.PREVIEW}
scrollState={scrollState.rendererScrollState}/>
}
containerClassName={'overflow-hidden'}/>
<Sidebar/>
</div> </div>
<ShowIf condition={!error && !loading}>
<div className={"flex-fill d-flex h-100 w-100 overflow-hidden flex-row"}>
<Splitter
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
left={
<EditorPane
onContentChange={setNoteMarkdownContent}
content={markdownContent}
scrollState={scrollState.editorScrollState}
onScroll={onEditorScroll}
onMakeScrollSource={setEditorToScrollSource}/>
}
showRight={editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH}
right={
<DocumentIframe
markdownContent={markdownContent}
onMakeScrollSource={setRendererToScrollSource}
onFirstHeadingChange={updateNoteTitleByFirstHeading}
onTaskCheckedChange={SetCheckboxInMarkdownContent}
onFrontmatterChange={setNoteFrontmatter}
onScroll={onMarkdownRendererScroll}
wide={editorMode === EditorMode.PREVIEW}
scrollState={scrollState.rendererScrollState}/>
}
containerClassName={'overflow-hidden'}/>
<Sidebar/>
</div>
</ShowIf>
</div> </div>
</Fragment> </Fragment>
) )

View file

@ -1,303 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { isTestMode } from '../../utils/is-test-mode'
export const editorTestContent = isTestMode() ? '' : `---
title: Features
description: Many features, such wow!
robots: noindex
tags:
- hedgedoc
- demo
- react
opengraph:
title: Features
---
# Embedding demo
[TOC]
## markmap
\`\`\`markmap
# MarkMap
## Pro
### written in typescript
## Cons
### must redeclare types
\`\`\`
## Vega-Lite
\`\`\`vega-lite
{
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"description": "Reproducing http://robslink.com/SAS/democd91/pyramid_pie.htm",
"data": {
"values": [
{"category": "Sky", "value": 75, "order": 3},
{"category": "Shady side of a pyramid", "value": 10, "order": 1},
{"category": "Sunny side of a pyramid", "value": 15, "order": 2}
]
},
"mark": {"type": "arc", "outerRadius": 80},
"encoding": {
"theta": {
"field": "value", "type": "quantitative",
"scale": {"range": [2.35619449, 8.639379797]},
"stack": true
},
"color": {
"field": "category", "type": "nominal",
"scale": {
"domain": ["Sky", "Shady side of a pyramid", "Sunny side of a pyramid"],
"range": ["#416D9D", "#674028", "#DEAC58"]
},
"legend": {
"orient": "none",
"title": null,
"columns": 1,
"legendX": 200,
"legendY": 80
}
},
"order": {
"field": "order"
}
},
"view": {"stroke": null}
}
\`\`\`
## GraphViz
\`\`\`graphviz
graph {
a -- b
a -- b
b -- a [color=blue]
}
\`\`\`
\`\`\`graphviz
digraph structs {
node [shape=record];
struct1 [label="<f0> left|<f1> mid&#92; dle|<f2> right"];
struct2 [label="<f0> one|<f1> two"];
struct3 [label="hello&#92;nworld |{ b |{c|<here> d|e}| f}| g | h"];
struct1:f1 -> struct2:f0;
struct1:f2 -> struct3:here;
}
\`\`\`
\`\`\`graphviz
digraph G {
main -> parse -> execute;
main -> init;
main -> cleanup;
execute -> make_string;
execute -> printf
init -> make_string;
main -> printf;
execute -> compare;
}
\`\`\`
\`\`\`graphviz
digraph D {
node [fontname="Arial"];
node_A [shape=record label="shape=record|{above|middle|below}|right"];
node_B [shape=plaintext label="shape=plaintext|{curly|braces and|bars without}|effect"];
}
\`\`\`
\`\`\`graphviz
digraph D {
A -> {B, C, D} -> {F}
}
\`\`\`
## High Res Image
![Wheat Field with Cypresses](/img/highres.jpg)
## Sequence Diagram (deprecated)
\`\`\`sequence
Title: Here is a title
note over A: asdd
A->B: Normal line
B-->C: Dashed line
C->>D: Open arrow
D-->>A: Dashed open arrow
participant IOOO
\`\`\`
## Mermaid
\`\`\`mermaid
gantt
title A Gantt Diagram
section Section
A task: a1, 2014-01-01, 30d
Another task: after a1, 20d
section Another
Task in sec: 2014-01-12, 12d
Another task: 24d
\`\`\`
## Flowchart
\`\`\`flow
st=>start: Start
e=>end: End
op=>operation: My Operation
op2=>operation: lalala
cond=>condition: Yes or No?
st->op->op2->cond
cond(yes)->e
cond(no)->op2
\`\`\`
## ABC
\`\`\`abc
X:1
T:Speed the Plough
M:4/4
C:Trad.
K:G
|:GABc dedB|dedB dedB|c2ec B2dB|c2A2 A2BA|
GABc dedB|dedB dedB|c2ec B2dB|A2F2 G4:|
|:g2gf gdBd|g2f2 e2d2|c2ec B2dB|c2A2 A2df|
g2gf g2Bd|g2f2 e2d2|c2ec B2dB|A2F2 G4:|
\`\`\`
## CSV
\`\`\`csv delimiter=; header
Username; Identifier;First name;Last name
"booker12; rbooker";9012;Rachel;Booker
grey07;2070;Laura;Grey
johnson81;4081;Craig;Johnson
jenkins46;9346;Mary;Jenkins
smith79;5079;Jamie;Smith
\`\`\`
## some plain text
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
## KaTeX
You can render *LaTeX* mathematical expressions using **KaTeX**, as on [math.stackexchange.com](https://math.stackexchange.com/):
The *Gamma function* satisfying $\\Gamma(n) = (n-1)!\\quad\\forall n\\in\\mathbb N$ is via the Euler integral
$$
x = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}.
$$
$$
\\Gamma(z) = \\int_0^\\infty t^{z-1}e^{-t}dt\\,.
$$
> More information about **LaTeX** mathematical expressions [here](https://meta.math.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference).
## Blockquote
> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
> [color=red] [name=John Doe] [time=2020-06-21 22:50]
## Slideshare
{%slideshare mazlan1/internet-of-things-the-tip-of-an-iceberg %}
## Gist
https://gist.github.com/schacon/1
## YouTube
https://www.youtube.com/watch?v=YE7VzlLtp-4
## Vimeo
https://vimeo.com/23237102
## Asciinema
https://asciinema.org/a/117928
## PDF
{%pdf https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf %}
## Code highlighting
\`\`\`js=
var s = "JavaScript syntax highlighting";
alert(s);
function $initHighlight(block, cls) {
try {
if (cls.search(/\\bno\\-highlight\\b/) != -1)
return process(block, true, 0x0F) +
' class=""';
} catch (e) {
/* handle exception */
}
for (var i = 0 / 2; i < classes.length; i++) {
if (checkCondition(classes[i]) === undefined)
return /\\d+[\\s/]/g;
}
}
\`\`\`
## PlantUML
\`\`\`plantuml
@startuml
participant Alice
participant "The **Famous** Bob" as Bob
Alice -> Bob : hello --there--
... Some ~~long delay~~ ...
Bob -> Alice : ok
note left
This is **bold**
This is //italics//
This is ""monospaced""
This is --stroked--
This is __underlined__
This is ~~waved~~
end note
Alice -> Bob : A //well formatted// message
note right of Alice
This is <back:cadetblue><size:18>displayed</size></back>
__left of__ Alice.
end note
note left of Bob
<u:red>This</u> is <color #118888>displayed</color>
**<color purple>left of</color> <s:red>Alice</strike> Bob**.
end note
note over Alice, Bob
<w:#FF33FF>This is hosted</w> by <img sourceforge.jpg>
end note
@enduml
\`\`\`
## ToDo List
- [ ] ToDos
- [X] Buy some salad
- [ ] Brush teeth
- [x] Drink some water
- [ ] **Click my box** and see the source code, if you're allowed to edit!
`

View file

@ -7,11 +7,11 @@
import yaml from 'js-yaml' import yaml from 'js-yaml'
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import frontmatter from 'markdown-it-front-matter' import frontmatter from 'markdown-it-front-matter'
import { RawYAMLMetadata, YAMLMetaData } from './yaml-metadata' import { NoteFrontmatter, RawNoteFrontmatter } from './note-frontmatter'
describe('yaml tests', () => { describe('yaml frontmatter tests', () => {
let raw: RawYAMLMetadata | undefined let raw: RawNoteFrontmatter | undefined
let finished: YAMLMetaData | undefined let finished: NoteFrontmatter | undefined
const md = new MarkdownIt('default', { const md = new MarkdownIt('default', {
html: true, html: true,
breaks: true, breaks: true,
@ -19,15 +19,15 @@ describe('yaml tests', () => {
typographer: true typographer: true
}) })
md.use(frontmatter, (rawMeta: string) => { md.use(frontmatter, (rawMeta: string) => {
raw = yaml.load(rawMeta) as RawYAMLMetadata raw = yaml.load(rawMeta) as RawNoteFrontmatter
finished = new YAMLMetaData(raw) finished = new NoteFrontmatter(raw)
}) })
// generate default YAMLMetadata // generate default YAMLMetadata
md.render('---\n---') md.render('---\n---')
const defaultYAML = finished const defaultYAML = finished
const testMetadata = (input: string, expectedRaw: Partial<RawYAMLMetadata>, expectedFinished: Partial<YAMLMetaData>) => { const testFrontmatter = (input: string, expectedRaw: Partial<RawNoteFrontmatter>, expectedFinished: Partial<NoteFrontmatter>) => {
md.render(input) md.render(input)
expect(raw).not.toBe(undefined) expect(raw).not.toBe(undefined)
expect(raw).toEqual(expectedRaw) expect(raw).toEqual(expectedRaw)
@ -44,47 +44,47 @@ describe('yaml tests', () => {
}) })
it('title only', () => { it('title only', () => {
testMetadata(`--- testFrontmatter(`---
title: test title: test
___ ___
`, `,
{ {
title: 'test' title: 'test'
}, },
{ {
title: 'test' title: 'test'
}) })
}) })
it('robots only', () => { it('robots only', () => {
testMetadata(`--- testFrontmatter(`---
robots: index, follow robots: index, follow
___ ___
`, `,
{ {
robots: 'index, follow' robots: 'index, follow'
}, },
{ {
robots: 'index, follow' robots: 'index, follow'
}) })
}) })
it('tags only (old syntax)', () => { it('tags only (old syntax)', () => {
testMetadata(`--- testFrontmatter(`---
tags: test123, abc tags: test123, abc
___ ___
`, `,
{ {
tags: 'test123, abc' tags: 'test123, abc'
}, },
{ {
tags: ['test123', 'abc'], tags: ['test123', 'abc'],
deprecatedTagsSyntax: true deprecatedTagsSyntax: true
}) })
}) })
it('tags only', () => { it('tags only', () => {
testMetadata(`--- testFrontmatter(`---
tags: tags:
- test123 - test123
- abc - abc
@ -100,7 +100,7 @@ describe('yaml tests', () => {
}) })
it('tags only (alternative syntax)', () => { it('tags only (alternative syntax)', () => {
testMetadata(`--- testFrontmatter(`---
tags: ['test123', 'abc'] tags: ['test123', 'abc']
___ ___
`, `,
@ -114,21 +114,21 @@ describe('yaml tests', () => {
}) })
it('breaks only', () => { it('breaks only', () => {
testMetadata(`--- testFrontmatter(`---
breaks: false breaks: false
___ ___
`, `,
{ {
breaks: false breaks: false
}, },
{ {
breaks: false breaks: false
}) })
}) })
/* /*
it('slideOptions nothing', () => { it('slideOptions nothing', () => {
testMetadata(`--- testFrontmatter(`---
slideOptions: slideOptions:
___ ___
`, `,
@ -144,7 +144,7 @@ describe('yaml tests', () => {
}) })
it('slideOptions.theme only', () => { it('slideOptions.theme only', () => {
testMetadata(`--- testFrontmatter(`---
slideOptions: slideOptions:
theme: sky theme: sky
___ ___
@ -164,7 +164,7 @@ describe('yaml tests', () => {
}) })
it('slideOptions full', () => { it('slideOptions full', () => {
testMetadata(`--- testFrontmatter(`---
slideOptions: slideOptions:
transition: zoom transition: zoom
theme: sky theme: sky
@ -186,46 +186,46 @@ describe('yaml tests', () => {
*/ */
it('opengraph nothing', () => { it('opengraph nothing', () => {
testMetadata(`--- testFrontmatter(`---
opengraph: opengraph:
___ ___
`, `,
{ {
opengraph: null opengraph: null
}, },
{ {
opengraph: new Map<string, string>() opengraph: new Map<string, string>()
}) })
}) })
it('opengraph title only', () => { it('opengraph title only', () => {
testMetadata(`--- testFrontmatter(`---
opengraph: opengraph:
title: Testtitle title: Testtitle
___ ___
`, `,
{ {
opengraph: { opengraph: {
title: 'Testtitle' title: 'Testtitle'
} }
}, },
{ {
opengraph: new Map<string, string>(Object.entries({ title: 'Testtitle' })) opengraph: new Map<string, string>(Object.entries({ title: 'Testtitle' }))
}) })
}) })
it('opengraph more attributes', () => { it('opengraph more attributes', () => {
testMetadata(`--- testFrontmatter(`---
opengraph: opengraph:
title: Testtitle title: Testtitle
image: https://dummyimage.com/48.png image: https://dummyimage.com/48.png
image:type: image/png image:type: image/png
___ ___
`, `,
{ {
opengraph: { opengraph: {
title: 'Testtitle', title: 'Testtitle',
image: 'https://dummyimage.com/48.png', image: 'https://dummyimage.com/48.png',
'image:type': 'image/png' 'image:type': 'image/png'
} }
}, },

View file

@ -8,7 +8,7 @@
type iso6391 = 'aa' | 'ab' | 'af' | 'am' | 'ar' | 'ar-ae' | 'ar-bh' | 'ar-dz' | 'ar-eg' | 'ar-iq' | 'ar-jo' | 'ar-kw' | 'ar-lb' | 'ar-ly' | 'ar-ma' | 'ar-om' | 'ar-qa' | 'ar-sa' | 'ar-sy' | 'ar-tn' | 'ar-ye' | 'as' | 'ay' | 'de-at' | 'de-ch' | 'de-li' | 'de-lu' | 'div' | 'dz' | 'el' | 'en' | 'en-au' | 'en-bz' | 'en-ca' | 'en-gb' | 'en-ie' | 'en-jm' | 'en-nz' | 'en-ph' | 'en-tt' | 'en-us' | 'en-za' | 'en-zw' | 'eo' | 'es' | 'es-ar' | 'es-bo' | 'es-cl' | 'es-co' | 'es-cr' | 'es-do' | 'es-ec' | 'es-es' | 'es-gt' | 'es-hn' | 'es-mx' | 'es-ni' | 'es-pa' | 'es-pe' | 'es-pr' | 'es-py' | 'es-sv' | 'es-us' | 'es-uy' | 'es-ve' | 'et' | 'eu' | 'fa' | 'fi' | 'fj' | 'fo' | 'fr' | 'fr-be' | 'fr-ca' | 'fr-ch' | 'fr-lu' | 'fr-mc' | 'fy' | 'ga' | 'gd' | 'gl' | 'gn' | 'gu' | 'ha' | 'he' | 'hi' | 'hr' | 'hu' | 'hy' | 'ia' | 'id' | 'ie' | 'ik' | 'in' | 'is' | 'it' | 'it-ch' | 'iw' | 'ja' | 'ji' | 'jw' | 'ka' | 'kk' | 'kl' | 'km' | 'kn' | 'ko' | 'kok' | 'ks' | 'ku' | 'ky' | 'kz' | 'la' | 'ln' | 'lo' | 'ls' | 'lt' | 'lv' | 'mg' | 'mi' | 'mk' | 'ml' | 'mn' | 'mo' | 'mr' | 'ms' | 'mt' | 'my' | 'na' | 'nb-no' | 'ne' | 'nl' | 'nl-be' | 'nn-no' | 'no' | 'oc' | 'om' | 'or' | 'pa' | 'pl' | 'ps' | 'pt' | 'pt-br' | 'qu' | 'rm' | 'rn' | 'ro' | 'ro-md' | 'ru' | 'ru-md' | 'rw' | 'sa' | 'sb' | 'sd' | 'sg' | 'sh' | 'si' | 'sk' | 'sl' | 'sm' | 'sn' | 'so' | 'sq' | 'sr' | 'ss' | 'st' | 'su' | 'sv' | 'sv-fi' | 'sw' | 'sx' | 'syr' | 'ta' | 'te' | 'tg' | 'th' | 'ti' | 'tk' | 'tl' | 'tn' | 'to' | 'tr' | 'ts' | 'tt' | 'tw' | 'uk' | 'ur' | 'us' | 'uz' | 'vi' | 'vo' | 'wo' | 'xh' | 'yi' | 'yo' | 'zh' | 'zh-cn' | 'zh-hk' | 'zh-mo' | 'zh-sg' | 'zh-tw' | 'zu' type iso6391 = 'aa' | 'ab' | 'af' | 'am' | 'ar' | 'ar-ae' | 'ar-bh' | 'ar-dz' | 'ar-eg' | 'ar-iq' | 'ar-jo' | 'ar-kw' | 'ar-lb' | 'ar-ly' | 'ar-ma' | 'ar-om' | 'ar-qa' | 'ar-sa' | 'ar-sy' | 'ar-tn' | 'ar-ye' | 'as' | 'ay' | 'de-at' | 'de-ch' | 'de-li' | 'de-lu' | 'div' | 'dz' | 'el' | 'en' | 'en-au' | 'en-bz' | 'en-ca' | 'en-gb' | 'en-ie' | 'en-jm' | 'en-nz' | 'en-ph' | 'en-tt' | 'en-us' | 'en-za' | 'en-zw' | 'eo' | 'es' | 'es-ar' | 'es-bo' | 'es-cl' | 'es-co' | 'es-cr' | 'es-do' | 'es-ec' | 'es-es' | 'es-gt' | 'es-hn' | 'es-mx' | 'es-ni' | 'es-pa' | 'es-pe' | 'es-pr' | 'es-py' | 'es-sv' | 'es-us' | 'es-uy' | 'es-ve' | 'et' | 'eu' | 'fa' | 'fi' | 'fj' | 'fo' | 'fr' | 'fr-be' | 'fr-ca' | 'fr-ch' | 'fr-lu' | 'fr-mc' | 'fy' | 'ga' | 'gd' | 'gl' | 'gn' | 'gu' | 'ha' | 'he' | 'hi' | 'hr' | 'hu' | 'hy' | 'ia' | 'id' | 'ie' | 'ik' | 'in' | 'is' | 'it' | 'it-ch' | 'iw' | 'ja' | 'ji' | 'jw' | 'ka' | 'kk' | 'kl' | 'km' | 'kn' | 'ko' | 'kok' | 'ks' | 'ku' | 'ky' | 'kz' | 'la' | 'ln' | 'lo' | 'ls' | 'lt' | 'lv' | 'mg' | 'mi' | 'mk' | 'ml' | 'mn' | 'mo' | 'mr' | 'ms' | 'mt' | 'my' | 'na' | 'nb-no' | 'ne' | 'nl' | 'nl-be' | 'nn-no' | 'no' | 'oc' | 'om' | 'or' | 'pa' | 'pl' | 'ps' | 'pt' | 'pt-br' | 'qu' | 'rm' | 'rn' | 'ro' | 'ro-md' | 'ru' | 'ru-md' | 'rw' | 'sa' | 'sb' | 'sd' | 'sg' | 'sh' | 'si' | 'sk' | 'sl' | 'sm' | 'sn' | 'so' | 'sq' | 'sr' | 'ss' | 'st' | 'su' | 'sv' | 'sv-fi' | 'sw' | 'sx' | 'syr' | 'ta' | 'te' | 'tg' | 'th' | 'ti' | 'tk' | 'tl' | 'tn' | 'to' | 'tr' | 'ts' | 'tt' | 'tw' | 'uk' | 'ur' | 'us' | 'uz' | 'vi' | 'vo' | 'wo' | 'xh' | 'yi' | 'yo' | 'zh' | 'zh-cn' | 'zh-hk' | 'zh-mo' | 'zh-sg' | 'zh-tw' | 'zu'
export interface RawYAMLMetadata { export interface RawNoteFrontmatter {
title: string | undefined title: string | undefined
description: string | undefined description: string | undefined
tags: string | string[] | undefined tags: string | string[] | undefined
@ -23,7 +23,7 @@ export interface RawYAMLMetadata {
opengraph: { [key: string]:string } | null opengraph: { [key: string]:string } | null
} }
export class YAMLMetaData { export class NoteFrontmatter {
title: string title: string
description: string description: string
tags: string[] tags: string[]
@ -38,7 +38,7 @@ export class YAMLMetaData {
// slideOptions: RevealOptions // slideOptions: RevealOptions
opengraph: Map<string, string> opengraph: Map<string, string>
constructor (rawData: RawYAMLMetadata) { constructor (rawData: RawNoteFrontmatter) {
this.title = rawData?.title ?? '' this.title = rawData?.title ?? ''
this.description = rawData?.description ?? '' this.description = rawData?.description ?? ''
this.robots = rawData?.robots ?? '' this.robots = rawData?.robots ?? ''
@ -46,9 +46,9 @@ export class YAMLMetaData {
this.GA = rawData?.GA ?? '' this.GA = rawData?.GA ?? ''
this.disqus = rawData?.disqus ?? '' this.disqus = rawData?.disqus ?? ''
this.type = (rawData?.type as YAMLMetaData['type']) ?? '' this.type = (rawData?.type as NoteFrontmatter['type']) ?? ''
this.lang = (rawData?.lang as iso6391) ?? 'en' this.lang = (rawData?.lang as iso6391) ?? 'en'
this.dir = (rawData?.dir as YAMLMetaData['dir']) ?? 'ltr' this.dir = (rawData?.dir as NoteFrontmatter['dir']) ?? 'ltr'
/* this.slideOptions = (rawData?.slideOptions as RevealOptions) ?? { /* this.slideOptions = (rawData?.slideOptions as RevealOptions) ?? {
transition: 'none', transition: 'none',

View file

@ -8,13 +8,13 @@ import { RefObject, useCallback } from 'react'
import { LineMarkerPosition } from '../../../markdown-renderer/types' import { LineMarkerPosition } from '../../../markdown-renderer/types'
import { ScrollState } from '../scroll-props' import { ScrollState } from '../scroll-props'
export const useOnUserScroll = (lineMarks: LineMarkerPosition[] | undefined, renderer: RefObject<HTMLElement>, onScroll: ((newScrollState: ScrollState) => void) | undefined): () => void => export const useOnUserScroll = (lineMarks: LineMarkerPosition[] | undefined, scrollContainer: RefObject<HTMLElement>, onScroll: ((newScrollState: ScrollState) => void) | undefined): () => void => {
useCallback(() => { return useCallback(() => {
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !onScroll) { if (!scrollContainer.current || !lineMarks || lineMarks.length === 0 || !onScroll) {
return return
} }
const scrollTop = renderer.current.scrollTop const scrollTop = scrollContainer.current.scrollTop
const lineMarksBeforeScrollTop = lineMarks.filter(lineMark => lineMark.position <= scrollTop) const lineMarksBeforeScrollTop = lineMarks.filter(lineMark => lineMark.position <= scrollTop)
if (lineMarksBeforeScrollTop.length === 0) { if (lineMarksBeforeScrollTop.length === 0) {
@ -44,4 +44,5 @@ export const useOnUserScroll = (lineMarks: LineMarkerPosition[] | undefined, ren
const newScrollState: ScrollState = { firstLineInView: line, scrolledPercentage: innerScrolling } const newScrollState: ScrollState = { firstLineInView: line, scrolledPercentage: innerScrolling }
onScroll(newScrollState) onScroll(newScrollState)
}, [lineMarks, onScroll, renderer]) }, [lineMarks, onScroll, scrollContainer])
}

View file

@ -9,21 +9,21 @@ import { LineMarkerPosition } from '../../../markdown-renderer/types'
import { ScrollState } from '../scroll-props' import { ScrollState } from '../scroll-props'
import { findLineMarks } from '../utils' import { findLineMarks } from '../utils'
export const useScrollToLineMark = (scrollState: ScrollState | undefined, lineMarks: LineMarkerPosition[] | undefined, contentLineCount: number, renderer: RefObject<HTMLElement>): void => { export const useScrollToLineMark = (scrollState: ScrollState | undefined, lineMarks: LineMarkerPosition[] | undefined, contentLineCount: number, scrollContainer: RefObject<HTMLElement>): void => {
const lastScrollPosition = useRef<number>() const lastScrollPosition = useRef<number>()
const scrollTo = useCallback((targetPosition: number): void => { const scrollTo = useCallback((targetPosition: number): void => {
if (!renderer.current || targetPosition === lastScrollPosition.current) { if (!scrollContainer.current || targetPosition === lastScrollPosition.current) {
return return
} }
lastScrollPosition.current = targetPosition lastScrollPosition.current = targetPosition
renderer.current.scrollTo({ scrollContainer.current.scrollTo({
top: targetPosition top: targetPosition
}) })
}, [renderer]) }, [scrollContainer])
useEffect(() => { useEffect(() => {
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !scrollState) { if (!scrollContainer.current || !lineMarks || lineMarks.length === 0 || !scrollState) {
return return
} }
if (scrollState.firstLineInView < lineMarks[0].line) { if (scrollState.firstLineInView < lineMarks[0].line) {
@ -31,12 +31,12 @@ export const useScrollToLineMark = (scrollState: ScrollState | undefined, lineMa
return return
} }
if (scrollState.firstLineInView > lineMarks[lineMarks.length - 1].line) { if (scrollState.firstLineInView > lineMarks[lineMarks.length - 1].line) {
scrollTo(renderer.current.offsetHeight) scrollTo(scrollContainer.current.offsetHeight)
return return
} }
const { lastMarkBefore, firstMarkAfter } = findLineMarks(lineMarks, scrollState.firstLineInView) const { lastMarkBefore, firstMarkAfter } = findLineMarks(lineMarks, scrollState.firstLineInView)
const positionBefore = lastMarkBefore ? lastMarkBefore.position : lineMarks[0].position const positionBefore = lastMarkBefore ? lastMarkBefore.position : lineMarks[0].position
const positionAfter = firstMarkAfter ? firstMarkAfter.position : renderer.current.offsetHeight const positionAfter = firstMarkAfter ? firstMarkAfter.position : scrollContainer.current.offsetHeight
const lastMarkBeforeLine = lastMarkBefore ? lastMarkBefore.line : 1 const lastMarkBeforeLine = lastMarkBefore ? lastMarkBefore.line : 1
const firstMarkAfterLine = firstMarkAfter ? firstMarkAfter.line : contentLineCount const firstMarkAfterLine = firstMarkAfter ? firstMarkAfter.line : contentLineCount
const linesBetweenMarkers = firstMarkAfterLine - lastMarkBeforeLine const linesBetweenMarkers = firstMarkAfterLine - lastMarkBeforeLine
@ -45,5 +45,5 @@ export const useScrollToLineMark = (scrollState: ScrollState | undefined, lineMa
const position = positionBefore + (scrollState.firstLineInView - lastMarkBeforeLine) * lineHeight + scrollState.scrolledPercentage / 100 * lineHeight const position = positionBefore + (scrollState.firstLineInView - lastMarkBeforeLine) * lineHeight + scrollState.scrolledPercentage / 100 * lineHeight
const correctedPosition = Math.floor(position) const correctedPosition = Math.floor(position)
scrollTo(correctedPosition) scrollTo(correctedPosition)
}, [contentLineCount, lineMarks, renderer, scrollState, scrollTo]) }, [contentLineCount, lineMarks, scrollContainer, scrollState, scrollTo])
} }

View file

@ -6,16 +6,14 @@
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { download } from '../../common/download/download' import { download } from '../../common/download/download'
import { SidebarButton } from './sidebar-button' import { SidebarButton } from './sidebar-button'
export const ExportMarkdownSidebarEntry: React.FC = () => { export const ExportMarkdownSidebarEntry: React.FC = () => {
useTranslation() useTranslation()
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content) const markdownContent = useNoteMarkdownContent()
const onClick = useCallback(() => { const onClick = useCallback(() => {
download(markdownContent, `title.md`, 'text/markdown') //todo: replace hard coded title with redux download(markdownContent, `title.md`, 'text/markdown') //todo: replace hard coded title with redux
}, [markdownContent]) }, [markdownContent])

View file

@ -6,14 +6,13 @@
import React, { Fragment, useCallback, useRef } from 'react' import React, { Fragment, useCallback, useRef } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
import { ApplicationState } from '../../../redux' import { setNoteMarkdownContent } from '../../../redux/note-details/methods'
import { setDocumentContent } from '../../../redux/document-content/methods'
import { SidebarButton } from './sidebar-button' import { SidebarButton } from './sidebar-button'
import { UploadInput } from './upload-input' import { UploadInput } from './upload-input'
export const ImportMarkdownSidebarEntry: React.FC = () => { export const ImportMarkdownSidebarEntry: React.FC = () => {
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content) const markdownContent = useNoteMarkdownContent()
useTranslation() useTranslation()
const onImportMarkdown = useCallback((file: File) => { const onImportMarkdown = useCallback((file: File) => {
@ -21,11 +20,7 @@ export const ImportMarkdownSidebarEntry: React.FC = () => {
const fileReader = new FileReader() const fileReader = new FileReader()
fileReader.addEventListener('load', () => { fileReader.addEventListener('load', () => {
const newContent = fileReader.result as string const newContent = fileReader.result as string
if (markdownContent.length === 0) { setNoteMarkdownContent(markdownContent.length === 0 ? newContent : `${markdownContent}\n${newContent}`)
setDocumentContent(newContent)
} else {
setDocumentContent(markdownContent + '\n' + newContent)
}
}) })
fileReader.addEventListener('loadend', () => { fileReader.addEventListener('loadend', () => {
resolve() resolve()
@ -39,15 +34,16 @@ export const ImportMarkdownSidebarEntry: React.FC = () => {
const clickRef = useRef<(() => void)>() const clickRef = useRef<(() => void)>()
const buttonClick = useCallback(() => { const buttonClick = useCallback(() => {
clickRef.current?.(); clickRef.current?.()
},[]); }, [])
return ( return (
<Fragment> <Fragment>
<SidebarButton data-cy={"menu-import-markdown"} icon={"file-text-o"} onClick={buttonClick}> <SidebarButton data-cy={"menu-import-markdown"} icon={"file-text-o"} onClick={buttonClick}>
<Trans i18nKey={'editor.import.file'}/> <Trans i18nKey={'editor.import.file'}/>
</SidebarButton> </SidebarButton>
<UploadInput onLoad={onImportMarkdown} data-cy={"menu-import-markdown-input"} acceptedFiles={'.md, text/markdown, text/plain'} onClickRef={clickRef}/> <UploadInput onLoad={onImportMarkdown} data-cy={"menu-import-markdown-input"}
acceptedFiles={'.md, text/markdown, text/plain'} onClickRef={clickRef}/>
</Fragment> </Fragment>
) )
} }

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect, useState } from 'react'
import { useParams } from 'react-router'
import { getNote } from '../../api/notes'
import { setNoteDataFromServer } from '../../redux/note-details/methods'
import { EditorPathParams } from './editor'
export const useLoadNoteFromServer = (): [boolean, boolean] => {
const { id } = useParams<EditorPathParams>()
const [error, setError] = useState(false)
const [loading, setLoading] = useState(true)
useEffect(() => {
getNote(id)
.then(note => {
setNoteDataFromServer(note)
})
.catch((e) => {
setError(true)
console.error(e)
})
.finally(() => setLoading(false))
}, [id])
return [error, loading]
}

View file

@ -10,10 +10,10 @@ import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { InternalLink } from '../common/links/internal-link' import { InternalLink } from '../common/links/internal-link'
import { ShowIf } from '../common/show-if/show-if' import { ShowIf } from '../common/show-if/show-if'
import { RawYAMLMetadata, YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata' import { NoteFrontmatter, RawNoteFrontmatter } from '../editor/note-frontmatter/note-frontmatter'
import { BasicMarkdownRenderer } from './basic-markdown-renderer' import { BasicMarkdownRenderer } from './basic-markdown-renderer'
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline' import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
import { usePostMetaDataOnChange } from './hooks/use-post-meta-data-on-change' import { usePostFrontmatterOnChange } from './hooks/use-post-frontmatter-on-change'
import { usePostTocAstOnChange } from './hooks/use-post-toc-ast-on-change' import { usePostTocAstOnChange } from './hooks/use-post-toc-ast-on-change'
import { useReplacerInstanceListCreator } from './hooks/use-replacer-instance-list-creator' import { useReplacerInstanceListCreator } from './hooks/use-replacer-instance-list-creator'
import { FullMarkdownItConfigurator } from './markdown-it-configurator/FullMarkdownItConfigurator' import { FullMarkdownItConfigurator } from './markdown-it-configurator/FullMarkdownItConfigurator'
@ -25,7 +25,7 @@ import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-po
export interface FullMarkdownRendererProps { export interface FullMarkdownRendererProps {
onFirstHeadingChange?: (firstHeading: string | undefined) => void onFirstHeadingChange?: (firstHeading: string | undefined) => void
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
onTocChange?: (ast: TocAst) => void onTocChange?: (ast: TocAst) => void
rendererRef?: Ref<HTMLDivElement> rendererRef?: Ref<HTMLDivElement>
@ -37,7 +37,7 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
{ {
onFirstHeadingChange, onFirstHeadingChange,
onLineMarkerPositionChanged, onLineMarkerPositionChanged,
onMetaDataChange, onFrontmatterChange,
onTaskCheckedChange, onTaskCheckedChange,
onTocChange, onTocChange,
content, content,
@ -53,11 +53,11 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
const [showYamlError, setShowYamlError] = useState(false) const [showYamlError, setShowYamlError] = useState(false)
const hasNewYamlError = useRef(false) const hasNewYamlError = useRef(false)
const rawMetaRef = useRef<RawYAMLMetadata>() const rawMetaRef = useRef<RawNoteFrontmatter>()
const firstHeadingRef = useRef<string>() const firstHeadingRef = useRef<string>()
const documentElement = useRef<HTMLDivElement>(null) const documentElement = useRef<HTMLDivElement>(null)
const currentLineMarkers = useRef<LineMarkers[]>() const currentLineMarkers = useRef<LineMarkers[]>()
usePostMetaDataOnChange(rawMetaRef.current, firstHeadingRef.current, onMetaDataChange, onFirstHeadingChange) usePostFrontmatterOnChange(rawMetaRef.current, firstHeadingRef.current, onFrontmatterChange, onFirstHeadingChange)
useCalculateLineMarkerPosition(documentElement, currentLineMarkers.current, onLineMarkerPositionChanged, documentElement.current?.offsetTop ?? 0) useCalculateLineMarkerPosition(documentElement, currentLineMarkers.current, onLineMarkerPositionChanged, documentElement.current?.offsetTop ?? 0)
useExtractFirstHeadline(documentElement, content, onFirstHeadingChange) useExtractFirstHeadline(documentElement, content, onFirstHeadingChange)
@ -66,7 +66,7 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
const markdownIt = useMemo(() => { const markdownIt = useMemo(() => {
return (new FullMarkdownItConfigurator( return (new FullMarkdownItConfigurator(
!!onMetaDataChange, !!onFrontmatterChange,
errorState => hasNewYamlError.current = errorState, errorState => hasNewYamlError.current = errorState,
rawMeta => { rawMeta => {
rawMetaRef.current = rawMeta rawMetaRef.current = rawMeta
@ -78,9 +78,9 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
currentLineMarkers.current = lineMarkers currentLineMarkers.current = lineMarkers
} }
)).buildConfiguredMarkdownIt() )).buildConfiguredMarkdownIt()
}, [onMetaDataChange]) }, [onFrontmatterChange])
const clearMetadata = useCallback(() => { const clearFrontmatter = useCallback(() => {
hasNewYamlError.current = false hasNewYamlError.current = false
rawMetaRef.current = undefined rawMetaRef.current = undefined
}, []) }, [])
@ -107,7 +107,7 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
componentReplacers={allReplacers} componentReplacers={allReplacers}
markdownIt={markdownIt} markdownIt={markdownIt}
documentReference={documentElement} documentReference={documentElement}
onBeforeRendering={clearMetadata} onBeforeRendering={clearFrontmatter}
onAfterRendering={checkYamlErrorState} onAfterRendering={checkYamlErrorState}
/> />
</div> </div>

View file

@ -4,10 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { useCallback, useEffect } from 'react' import React, { useCallback, useEffect, useRef } from 'react'
export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDivElement>, content: string, onFirstHeadingChange?: (firstHeading: string | undefined) => void): void => { export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDivElement>, content: string, onFirstHeadingChange?: (firstHeading: string | undefined) => void): void => {
const extractInnerText = useCallback((node: ChildNode): string => { const extractInnerText = useCallback((node: ChildNode | null): string => {
if (!node) {
return ''
}
let innerText = '' let innerText = ''
if ((node as HTMLElement).classList?.contains("katex-mathml")) { if ((node as HTMLElement).classList?.contains("katex-mathml")) {
@ -15,7 +19,9 @@ export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDiv
} }
if (node.childNodes && node.childNodes.length > 0) { if (node.childNodes && node.childNodes.length > 0) {
node.childNodes.forEach((child) => { innerText += extractInnerText(child) }) node.childNodes.forEach((child) => {
innerText += extractInnerText(child)
})
} else if (node.nodeName === 'IMG') { } else if (node.nodeName === 'IMG') {
innerText += (node as HTMLImageElement).getAttribute('alt') innerText += (node as HTMLImageElement).getAttribute('alt')
} else { } else {
@ -24,14 +30,17 @@ export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDiv
return innerText return innerText
}, []) }, [])
const lastFirstHeading = useRef<string | undefined>()
useEffect(() => { useEffect(() => {
if (onFirstHeadingChange && documentElement.current) { if (onFirstHeadingChange && documentElement.current) {
const firstHeading = documentElement.current.getElementsByTagName('h1').item(0) const firstHeading = documentElement.current.getElementsByTagName('h1').item(0)
if (firstHeading) { const headingText = extractInnerText(firstHeading)
onFirstHeadingChange(extractInnerText(firstHeading)) if (headingText === lastFirstHeading.current) {
} else { return
onFirstHeadingChange(undefined)
} }
lastFirstHeading.current = headingText
onFirstHeadingChange(headingText)
} }
}, [documentElement, extractInnerText, onFirstHeadingChange, content]) }, [documentElement, extractInnerText, onFirstHeadingChange, content])
} }

View file

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import equal from 'fast-deep-equal'
import { useEffect, useRef } from 'react'
import { NoteFrontmatter, RawNoteFrontmatter } from '../../editor/note-frontmatter/note-frontmatter'
export const usePostFrontmatterOnChange = (
rawFrontmatter: RawNoteFrontmatter | undefined,
firstHeadingRef: string | undefined,
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void,
onFirstHeadingChange?: (firstHeading: string | undefined) => void
): void => {
const oldMetaRef = useRef<RawNoteFrontmatter>()
const oldFirstHeadingRef = useRef<string>()
useEffect(() => {
if (onFrontmatterChange && !equal(oldMetaRef.current, rawFrontmatter)) {
if (rawFrontmatter) {
const newFrontmatter = new NoteFrontmatter(rawFrontmatter)
onFrontmatterChange(newFrontmatter)
} else {
onFrontmatterChange(undefined)
}
oldMetaRef.current = rawFrontmatter
}
if (onFirstHeadingChange && !equal(firstHeadingRef, oldFirstHeadingRef.current)) {
onFirstHeadingChange(firstHeadingRef || undefined)
oldFirstHeadingRef.current = firstHeadingRef
}
})
}

View file

@ -1,35 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import equal from 'fast-deep-equal'
import { useEffect, useRef } from 'react'
import { RawYAMLMetadata, YAMLMetaData } from '../../editor/yaml-metadata/yaml-metadata'
export const usePostMetaDataOnChange = (
rawMetaRef: RawYAMLMetadata|undefined,
firstHeadingRef: string|undefined,
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void,
onFirstHeadingChange?: (firstHeading: string | undefined) => void
): void => {
const oldMetaRef = useRef<RawYAMLMetadata>()
const oldFirstHeadingRef = useRef<string>()
useEffect(() => {
if (onMetaDataChange && !equal(oldMetaRef.current, rawMetaRef)) {
if (rawMetaRef) {
const newMetaData = new YAMLMetaData(rawMetaRef)
onMetaDataChange(newMetaData)
} else {
onMetaDataChange(undefined)
}
oldMetaRef.current = rawMetaRef
}
if (onFirstHeadingChange && !equal(firstHeadingRef, oldFirstHeadingRef.current)) {
onFirstHeadingChange(firstHeadingRef || undefined)
oldFirstHeadingRef.current = firstHeadingRef
}
})
}

View file

@ -6,7 +6,7 @@
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import { TocAst } from 'markdown-it-toc-done-right' import { TocAst } from 'markdown-it-toc-done-right'
import { RawYAMLMetadata } from '../../editor/yaml-metadata/yaml-metadata' import { RawNoteFrontmatter } from '../../editor/note-frontmatter/note-frontmatter'
import { documentToc } from '../markdown-it-plugins/document-toc' import { documentToc } from '../markdown-it-plugins/document-toc'
import { frontmatterExtract } from '../markdown-it-plugins/frontmatter' import { frontmatterExtract } from '../markdown-it-plugins/frontmatter'
import { headlineAnchors } from '../markdown-it-plugins/headline-anchors' import { headlineAnchors } from '../markdown-it-plugins/headline-anchors'
@ -28,7 +28,7 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
constructor ( constructor (
private useFrontmatter: boolean, private useFrontmatter: boolean,
private passYamlErrorState: (error: boolean) => void, private passYamlErrorState: (error: boolean) => void,
private onRawMeta: (rawMeta: RawYAMLMetadata) => 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
) { ) {
@ -45,8 +45,8 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
!this.useFrontmatter !this.useFrontmatter
? undefined ? undefined
: { : {
onYamlError: (hasError: boolean) => this.passYamlErrorState(hasError), onParseError: (hasError: boolean) => this.passYamlErrorState(hasError),
onRawMeta: (rawMeta: RawYAMLMetadata) => this.onRawMeta(rawMeta) onRawMeta: (rawMeta: RawNoteFrontmatter) => this.onRawMeta(rawMeta)
}) })
}, },
headlineAnchors, headlineAnchors,

View file

@ -7,11 +7,11 @@
import yaml from 'js-yaml' import yaml from 'js-yaml'
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import frontmatter from 'markdown-it-front-matter' import frontmatter from 'markdown-it-front-matter'
import { RawYAMLMetadata } from '../../editor/yaml-metadata/yaml-metadata' import { RawNoteFrontmatter } from '../../editor/note-frontmatter/note-frontmatter'
interface FrontmatterPluginOptions { interface FrontmatterPluginOptions {
onYamlError: (error: boolean) => void, onParseError: (error: boolean) => void,
onRawMeta: (rawMeta: RawYAMLMetadata) => void, onRawMeta: (rawMeta: RawNoteFrontmatter) => void,
} }
export const frontmatterExtract: MarkdownIt.PluginWithOptions<FrontmatterPluginOptions> = (markdownIt: MarkdownIt, options) => { export const frontmatterExtract: MarkdownIt.PluginWithOptions<FrontmatterPluginOptions> = (markdownIt: MarkdownIt, options) => {
@ -20,13 +20,13 @@ export const frontmatterExtract: MarkdownIt.PluginWithOptions<FrontmatterPluginO
} }
frontmatter(markdownIt, (rawMeta: string) => { frontmatter(markdownIt, (rawMeta: string) => {
try { try {
const meta: RawYAMLMetadata = yaml.load(rawMeta) as RawYAMLMetadata const meta: RawNoteFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter
options.onYamlError(false) options.onParseError(false)
options.onRawMeta(meta) options.onRawMeta(meta)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
options.onYamlError(true) options.onParseError(true)
options.onRawMeta({} as RawYAMLMetadata) options.onRawMeta({} as RawNoteFrontmatter)
} }
}) })
} }

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ShowIf } from '../common/show-if/show-if'
export interface ErrorWhileLoadingNoteAlertProps {
show: boolean
}
export const ErrorWhileLoadingNoteAlert: React.FC<ErrorWhileLoadingNoteAlertProps> = ({ show }) => {
useTranslation()
return (
<ShowIf condition={show}>
<Alert variant={'danger'} className={'my-2'}>
<b><Trans i18nKey={'views.readOnly.error.title'}/></b>
<br/>
<Trans i18nKey={'views.readOnly.error.description'}/>
</Alert>
</ShowIf>
)
}

View file

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { Alert } from 'react-bootstrap'
import { Trans } from 'react-i18next'
import { ShowIf } from '../common/show-if/show-if'
export interface LoadingNoteAlertProps {
show: boolean
}
export const LoadingNoteAlert: React.FC<LoadingNoteAlertProps> = ({ show }) => {
return (
<ShowIf condition={show}>
<Alert variant={'info'} className={'my-2'}>
<Trans i18nKey={'views.readOnly.loading'}/>
</Alert>
</ShowIf>
)
}

View file

@ -1,8 +1,8 @@
/* /*
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 { DateTime } from 'luxon' import { DateTime } from 'luxon'
import React from 'react' import React from 'react'
@ -17,9 +17,9 @@ import './document-infobar.scss'
export interface DocumentInfobarProps { export interface DocumentInfobarProps {
changedAuthor: string changedAuthor: string
changedTime: number changedTime: DateTime
createdAuthor: string createdAuthor: string
createdTime: number createdTime: DateTime
editable: boolean editable: boolean
noteId: string noteId: string
viewCount: number viewCount: number
@ -43,12 +43,12 @@ export const DocumentInfobar: React.FC<DocumentInfobarProps> = ({
<div className={'d-flex flex-column'}> <div className={'d-flex flex-column'}>
<DocumentInfoTimeLine <DocumentInfoTimeLine
mode={DocumentInfoLineWithTimeMode.CREATED} mode={DocumentInfoLineWithTimeMode.CREATED}
time={ DateTime.fromSeconds(createdTime) } time={createdTime}
userName={createdAuthor} userName={createdAuthor}
profileImageSrc={'/img/avatar.png'}/> profileImageSrc={'/img/avatar.png'}/>
<DocumentInfoTimeLine <DocumentInfoTimeLine
mode={DocumentInfoLineWithTimeMode.EDITED} mode={DocumentInfoLineWithTimeMode.EDITED}
time={ DateTime.fromSeconds(changedTime) } time={changedTime}
userName={changedAuthor} userName={changedAuthor}
profileImageSrc={'/img/avatar.png'}/> profileImageSrc={'/img/avatar.png'}/>
<hr/> <hr/>

View file

@ -4,99 +4,61 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback } from 'react'
import { Alert } from 'react-bootstrap' import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { useParams } from 'react-router' import { useParams } from 'react-router'
import { getNote, Note } from '../../api/notes'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode' import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { useDocumentTitle } from '../../hooks/common/use-document-title' import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
import { ApplicationState } from '../../redux' import { ApplicationState } from '../../redux'
import { setDocumentContent, setDocumentMetadata } from '../../redux/document-content/methods' import { setNoteFrontmatter, updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
import { MotdBanner } from '../common/motd-banner/motd-banner' import { MotdBanner } from '../common/motd-banner/motd-banner'
import { ShowIf } from '../common/show-if/show-if' import { ShowIf } from '../common/show-if/show-if'
import { AppBar, AppBarMode } from '../editor/app-bar/app-bar' import { AppBar, AppBarMode } from '../editor/app-bar/app-bar'
import { DocumentIframe } from '../editor/document-renderer-pane/document-iframe' import { DocumentIframe } from '../editor/document-renderer-pane/document-iframe'
import { EditorPathParams } from '../editor/editor' import { EditorPathParams } from '../editor/editor'
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata' import { useLoadNoteFromServer } from '../editor/useLoadNoteFromServer'
import { DocumentInfobar } from './document-infobar' import { DocumentInfobar } from './document-infobar'
import { ErrorWhileLoadingNoteAlert } from './ErrorWhileLoadingNoteAlert'
import { LoadingNoteAlert } from './LoadingNoteAlert'
export const PadViewOnly: React.FC = () => { export const PadViewOnly: React.FC = () => {
const { t } = useTranslation()
useTranslation()
const { id } = useParams<EditorPathParams>() const { id } = useParams<EditorPathParams>()
const untitledNote = t('editor.untitledNote')
const [documentTitle, setDocumentTitle] = useState(untitledNote)
const [noteData, setNoteData] = useState<Note>()
const [error, setError] = useState(false)
const [loading, setLoading] = useState(true)
const noteMetadata = useRef<YAMLMetaData>()
const firstHeading = useRef<string>()
const updateDocumentTitle = useCallback(() => {
const noteTitle = extractNoteTitle(untitledNote, noteMetadata.current, firstHeading.current)
setDocumentTitle(noteTitle)
}, [untitledNote])
const onFirstHeadingChange = useCallback((newFirstHeading: string | undefined) => {
firstHeading.current = newFirstHeading
updateDocumentTitle()
}, [updateDocumentTitle])
const onMetadataChange = useCallback((metaData: YAMLMetaData | undefined) => {
noteMetadata.current = metaData
setDocumentMetadata(metaData)
updateDocumentTitle()
}, [updateDocumentTitle])
useEffect(() => {
getNote(id)
.then(note => {
setNoteData(note)
setDocumentContent(note.content)
})
.catch(() => setError(true))
.finally(() => setLoading(false))
}, [id])
useApplyDarkMode() useApplyDarkMode()
useDocumentTitle(documentTitle) useDocumentTitleWithNoteTitle()
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
const onFirstHeadingChange = useCallback(updateNoteTitleByFirstHeading, [])
const onFrontmatterChange = useCallback(setNoteFrontmatter, [])
const [error, loading] = useLoadNoteFromServer()
const markdownContent = useNoteMarkdownContent()
const noteDetails = useSelector((state: ApplicationState) => state.noteDetails)
return ( return (
<div className={'d-flex flex-column mvh-100 bg-light'}> <div className={'d-flex flex-column mvh-100 bg-light'}>
<MotdBanner/> <MotdBanner/>
<AppBar mode={AppBarMode.BASIC}/> <AppBar mode={AppBarMode.BASIC}/>
<div className={'container'}> <div className={'container'}>
<ShowIf condition={error}> <ErrorWhileLoadingNoteAlert show={error}/>
<Alert variant={'danger'} className={'my-2'}> <LoadingNoteAlert show={loading}/>
<b><Trans i18nKey={'views.readOnly.error.title'}/></b>
<br/>
<Trans i18nKey={'views.readOnly.error.description'}/>
</Alert>
</ShowIf>
<ShowIf condition={loading}>
<Alert variant={'info'} className={'my-2'}>
<Trans i18nKey={'views.readOnly.loading'}/>
</Alert>
</ShowIf>
</div> </div>
<ShowIf condition={!error && !loading}> <ShowIf condition={!error && !loading}>
{ /* TODO set editable and created author properly */}
<DocumentInfobar <DocumentInfobar
changedAuthor={noteData?.lastChange.userId ?? ''} changedAuthor={noteDetails.lastChange.userId ?? ''}
changedTime={noteData?.lastChange.timestamp ?? 0} changedTime={noteDetails.lastChange.timestamp}
createdAuthor={'Test'} createdAuthor={'Test'}
createdTime={noteData?.createtime ?? 0} createdTime={noteDetails.createTime}
editable={true} editable={true}
noteId={id} noteId={id}
viewCount={noteData?.viewcount ?? 0} viewCount={noteDetails.viewCount}
/> />
<DocumentIframe extraClasses={"flex-fill"} <DocumentIframe extraClasses={"flex-fill"}
markdownContent={markdownContent} markdownContent={markdownContent}
onFirstHeadingChange={onFirstHeadingChange} onFirstHeadingChange={onFirstHeadingChange}
onMetadataChange={onMetadataChange}/> onFrontmatterChange={onFrontmatterChange}/>
</ShowIf> </ShowIf>
</div> </div>
) )

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { NoteFrontmatter } from "../editor/note-frontmatter/note-frontmatter"
import { ScrollState } from "../editor/scroll/scroll-props" import { ScrollState } from "../editor/scroll/scroll-props"
import { YAMLMetaData } from "../editor/yaml-metadata/yaml-metadata"
import { IframeCommunicator } from "./iframe-communicator" import { IframeCommunicator } from "./iframe-communicator"
import { import {
EditorToRendererIframeMessage, EditorToRendererIframeMessage,
@ -18,36 +18,13 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<Edito
private onSetScrollSourceToRendererHandler?: () => void private onSetScrollSourceToRendererHandler?: () => void
private onTaskCheckboxChangeHandler?: (lineInMarkdown: number, checked: boolean) => void private onTaskCheckboxChangeHandler?: (lineInMarkdown: number, checked: boolean) => void
private onFirstHeadingChangeHandler?: (heading?: string) => void private onFirstHeadingChangeHandler?: (heading?: string) => void
private onMetaDataChangeHandler?: (metaData?: YAMLMetaData) => void private onFrontmatterChangeHandler?: (metaData?: NoteFrontmatter) => void
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
protected handleEvent (event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined { public onFrontmatterChange (handler?: (frontmatter?: NoteFrontmatter) => void): void {
const renderMessage = event.data this.onFrontmatterChangeHandler = handler
switch (renderMessage.type) {
case RenderIframeMessageType.RENDERER_READY:
this.onRendererReadyHandler?.()
return false
case RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER:
this.onSetScrollSourceToRendererHandler?.()
return false
case RenderIframeMessageType.SET_SCROLL_STATE:
this.onSetScrollStateHandler?.(renderMessage.scrollState)
return false
case RenderIframeMessageType.ON_FIRST_HEADING_CHANGE:
this.onFirstHeadingChangeHandler?.(renderMessage.firstHeading)
return false
case RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE:
this.onTaskCheckboxChangeHandler?.(renderMessage.lineInMarkdown, renderMessage.checked)
return false
case RenderIframeMessageType.ON_SET_META_DATA:
this.onMetaDataChangeHandler?.(renderMessage.metaData)
return false
case RenderIframeMessageType.IMAGE_CLICKED:
this.onImageClickedHandler?.(renderMessage.details)
return false
}
} }
public onImageClicked (handler?: (details: ImageDetails) => void): void { public onImageClicked (handler?: (details: ImageDetails) => void): void {
@ -70,8 +47,31 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<Edito
this.onFirstHeadingChangeHandler = handler this.onFirstHeadingChangeHandler = handler
} }
public onMetaDataChange (handler?: (metaData?: YAMLMetaData) => void): void { protected handleEvent (event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined {
this.onMetaDataChangeHandler = handler const renderMessage = event.data
switch (renderMessage.type) {
case RenderIframeMessageType.RENDERER_READY:
this.onRendererReadyHandler?.()
return false
case RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER:
this.onSetScrollSourceToRendererHandler?.()
return false
case RenderIframeMessageType.SET_SCROLL_STATE:
this.onSetScrollStateHandler?.(renderMessage.scrollState)
return false
case RenderIframeMessageType.ON_FIRST_HEADING_CHANGE:
this.onFirstHeadingChangeHandler?.(renderMessage.firstHeading)
return false
case RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE:
this.onTaskCheckboxChangeHandler?.(renderMessage.lineInMarkdown, renderMessage.checked)
return false
case RenderIframeMessageType.ON_SET_FRONTMATTER:
this.onFrontmatterChangeHandler?.(renderMessage.frontmatter)
return false
case RenderIframeMessageType.IMAGE_CLICKED:
this.onImageClickedHandler?.(renderMessage.details)
return false
}
} }
public onSetScrollState (handler?: (scrollState: ScrollState) => void): void { public onSetScrollState (handler?: (scrollState: ScrollState) => void): void {

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { NoteFrontmatter } from "../editor/note-frontmatter/note-frontmatter"
import { ScrollState } from "../editor/scroll/scroll-props" import { ScrollState } from "../editor/scroll/scroll-props"
import { YAMLMetaData } from "../editor/yaml-metadata/yaml-metadata"
import { IframeCommunicator } from "./iframe-communicator" import { IframeCommunicator } from "./iframe-communicator"
import { import {
EditorToRendererIframeMessage, EditorToRendererIframeMessage,
@ -68,10 +68,10 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<Rende
}) })
} }
public sendSetMetaData (metaData: YAMLMetaData | undefined): void { public sendSetFrontmatter (frontmatter: NoteFrontmatter | undefined): void {
this.sendMessageToOtherSide({ this.sendMessageToOtherSide({
type: RenderIframeMessageType.ON_SET_META_DATA, type: RenderIframeMessageType.ON_SET_FRONTMATTER,
metaData frontmatter: frontmatter
}) })
} }

View file

@ -3,16 +3,15 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import equal from "fast-deep-equal"
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode' import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { ApplicationState } from '../../redux' import { ApplicationState } from '../../redux'
import { setDarkMode } from '../../redux/dark-mode/methods' import { setDarkMode } from '../../redux/dark-mode/methods'
import { setDocumentMetadata } from '../../redux/document-content/methods' import { setNoteFrontmatter } from '../../redux/note-details/methods'
import { ScrollingDocumentRenderPane } from '../editor/document-renderer-pane/scrolling-document-render-pane' import { DocumentRenderPane } from '../editor/document-renderer-pane/document-render-pane'
import { NoteFrontmatter } from '../editor/note-frontmatter/note-frontmatter'
import { ScrollState } from '../editor/scroll/scroll-props' import { ScrollState } from '../editor/scroll/scroll-props'
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer' import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
import { IframeRendererToEditorCommunicator } from './iframe-renderer-to-editor-communicator' import { IframeRendererToEditorCommunicator } from './iframe-renderer-to-editor-communicator'
@ -41,11 +40,7 @@ export const RenderPage: React.FC = () => {
useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator]) useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator]) useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetWide(setWide), [iframeCommunicator]) useEffect(() => iframeCommunicator.onSetWide(setWide), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetScrollState((newScrollState) => { useEffect(() => iframeCommunicator.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
if (!equal(scrollState, newScrollState)) {
setScrollState(newScrollState)
}
}), [iframeCommunicator, scrollState])
const onTaskCheckedChange = useCallback((lineInMarkdown: number, checked: boolean) => { const onTaskCheckedChange = useCallback((lineInMarkdown: number, checked: boolean) => {
iframeCommunicator.sendTaskCheckBoxChange(lineInMarkdown, checked) iframeCommunicator.sendTaskCheckBoxChange(lineInMarkdown, checked)
@ -59,9 +54,9 @@ export const RenderPage: React.FC = () => {
iframeCommunicator.sendSetScrollSourceToRenderer() iframeCommunicator.sendSetScrollSourceToRenderer()
}, [iframeCommunicator]) }, [iframeCommunicator])
const onMetaDataChange = useCallback((metaData?: YAMLMetaData) => { const onFrontmatterChange = useCallback((frontmatter?: NoteFrontmatter) => {
setDocumentMetadata(metaData) setNoteFrontmatter(frontmatter)
iframeCommunicator.sendSetMetaData(metaData) iframeCommunicator.sendSetFrontmatter(frontmatter)
}, [iframeCommunicator]) }, [iframeCommunicator])
const onScroll = useCallback((scrollState: ScrollState) => { const onScroll = useCallback((scrollState: ScrollState) => {
@ -86,14 +81,14 @@ export const RenderPage: React.FC = () => {
return ( return (
<div className={"vh-100 w-100"}> <div className={"vh-100 w-100"}>
<ScrollingDocumentRenderPane <DocumentRenderPane
extraClasses={'w-100'} extraClasses={'w-100'}
markdownContent={markdownContent} markdownContent={markdownContent}
wide={isWide} wide={isWide}
onTaskCheckedChange={onTaskCheckedChange} onTaskCheckedChange={onTaskCheckedChange}
onFirstHeadingChange={onFirstHeadingChange} onFirstHeadingChange={onFirstHeadingChange}
onMakeScrollSource={onMakeScrollSource} onMakeScrollSource={onMakeScrollSource}
onMetadataChange={onMetaDataChange} onFrontmatterChange={onFrontmatterChange}
scrollState={scrollState} scrollState={scrollState}
onScroll={onScroll} onScroll={onScroll}
baseUrl={baseUrl} baseUrl={baseUrl}

View file

@ -3,8 +3,8 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { NoteFrontmatter } from '../editor/note-frontmatter/note-frontmatter'
import { ScrollState } from '../editor/scroll/scroll-props' import { ScrollState } from '../editor/scroll/scroll-props'
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
export enum RenderIframeMessageType { export enum RenderIframeMessageType {
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT', SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
@ -15,7 +15,7 @@ export enum RenderIframeMessageType {
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE', ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
SET_SCROLL_SOURCE_TO_RENDERER = 'SET_SCROLL_SOURCE_TO_RENDERER', SET_SCROLL_SOURCE_TO_RENDERER = 'SET_SCROLL_SOURCE_TO_RENDERER',
SET_SCROLL_STATE = 'SET_SCROLL_STATE', SET_SCROLL_STATE = 'SET_SCROLL_STATE',
ON_SET_META_DATA = 'ON_SET_META_DATA', ON_SET_FRONTMATTER = 'ON_SET_FRONTMATTER',
IMAGE_CLICKED = 'IMAGE_CLICKED', IMAGE_CLICKED = 'IMAGE_CLICKED',
SET_BASE_URL = 'SET_BASE_URL' SET_BASE_URL = 'SET_BASE_URL'
} }
@ -71,9 +71,9 @@ export interface OnFirstHeadingChangeMessage {
firstHeading: string | undefined firstHeading: string | undefined
} }
export interface OnMetadataChangeMessage { export interface OnFrontmatterChangeMessage {
type: RenderIframeMessageType.ON_SET_META_DATA, type: RenderIframeMessageType.ON_SET_FRONTMATTER,
metaData: YAMLMetaData | undefined frontmatter: NoteFrontmatter | undefined
} }
export type EditorToRendererIframeMessage = export type EditorToRendererIframeMessage =
@ -87,6 +87,6 @@ export type RendererToEditorIframeMessage =
RendererToEditorSimpleMessage | RendererToEditorSimpleMessage |
OnFirstHeadingChangeMessage | OnFirstHeadingChangeMessage |
OnTaskCheckboxChangeMessage | OnTaskCheckboxChangeMessage |
OnMetadataChangeMessage | OnFrontmatterChangeMessage |
SetScrollStateMessage | SetScrollStateMessage |
ImageClickedMessage ImageClickedMessage

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import { useDocumentTitle } from './use-document-title'
export const useDocumentTitleWithNoteTitle = (): void => {
const { t } = useTranslation()
const untitledNote = t('editor.untitledNote')
const noteTitle = useSelector((state: ApplicationState) => state.noteDetails.noteTitle)
useDocumentTitle(noteTitle === '' ? untitledNote : noteTitle)
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
export const useNoteMarkdownContent = (): string => {
return useSelector((state: ApplicationState) => state.noteDetails.markdownContent)
}

View file

@ -1,8 +1,8 @@
/* /*
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'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
@ -86,7 +86,7 @@ ReactDOM.render(
) )
if (isTestMode()) { if (isTestMode()) {
console.log("This build runs in test mode. This means:\n - No default content in the editor\n - no sandboxed iframe") console.log("This build runs in test mode. This means:\n - no sandboxed iframe")
} }
// If you want your app to work offline and load faster, you can change // If you want your app to work offline and load faster, you can change

View file

@ -1,42 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { store } from '..'
import { YAMLMetaData } from '../../components/editor/yaml-metadata/yaml-metadata'
import { initialState } from './reducers'
import {
DocumentContentActionType,
SetDocumentContentAction,
SetDocumentMetadataAction,
SetNoteIdAction
} from './types'
export const setDocumentContent = (content: string): void => {
const action: SetDocumentContentAction = {
type: DocumentContentActionType.SET_DOCUMENT_CONTENT,
content
}
store.dispatch(action)
}
export const setNoteId = (noteId: string): void => {
const action: SetNoteIdAction = {
type: DocumentContentActionType.SET_NOTE_ID,
noteId
}
store.dispatch(action)
}
export const setDocumentMetadata = (metadata: YAMLMetaData | undefined): void => {
if (!metadata) {
metadata = initialState.metadata
}
const action: SetDocumentMetadataAction = {
type: DocumentContentActionType.SET_DOCUMENT_METADATA,
metadata
}
store.dispatch(action)
}

View file

@ -1,56 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Reducer } from 'redux'
import {
DocumentContent,
DocumentContentAction,
DocumentContentActionType,
SetDocumentContentAction,
SetDocumentMetadataAction,
SetNoteIdAction
} from './types'
export const initialState: DocumentContent = {
content: '',
noteId: '',
metadata: {
title: '',
description: '',
tags: [],
deprecatedTagsSyntax: false,
robots: '',
lang: 'en',
dir: 'ltr',
breaks: true,
GA: '',
disqus: '',
type: '',
opengraph: new Map<string, string>()
}
}
export const DocumentContentReducer: Reducer<DocumentContent, DocumentContentAction> = (state: DocumentContent = initialState, action: DocumentContentAction) => {
switch (action.type) {
case DocumentContentActionType.SET_DOCUMENT_CONTENT:
return {
...state,
content: (action as SetDocumentContentAction).content
}
case DocumentContentActionType.SET_NOTE_ID:
return {
...state,
noteId: (action as SetNoteIdAction).noteId
}
case DocumentContentActionType.SET_DOCUMENT_METADATA:
return {
...state,
metadata: (action as SetDocumentMetadataAction).metadata
}
default:
return state
}
}

View file

@ -1,36 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Action } from 'redux'
import { YAMLMetaData } from '../../components/editor/yaml-metadata/yaml-metadata'
export enum DocumentContentActionType {
SET_DOCUMENT_CONTENT = 'document-content/set',
SET_NOTE_ID = 'document-content/noteid/set',
SET_DOCUMENT_METADATA = 'document-content/metadata/set'
}
export interface DocumentContent {
content: string
noteId: string,
metadata: YAMLMetaData
}
export interface DocumentContentAction extends Action<DocumentContentActionType> {
type: DocumentContentActionType
}
export interface SetDocumentContentAction extends DocumentContentAction {
content: string
}
export interface SetNoteIdAction extends DocumentContentAction {
noteId: string
}
export interface SetDocumentMetadataAction extends DocumentContentAction {
metadata: YAMLMetaData
}

View file

@ -13,10 +13,10 @@ import { BannerState } from './banner/types'
import { ConfigReducer } from './config/reducers' import { ConfigReducer } from './config/reducers'
import { DarkModeConfigReducer } from './dark-mode/reducers' import { DarkModeConfigReducer } from './dark-mode/reducers'
import { DarkModeConfig } from './dark-mode/types' import { DarkModeConfig } from './dark-mode/types'
import { DocumentContentReducer } from './document-content/reducers'
import { DocumentContent } from './document-content/types'
import { EditorConfigReducer } from './editor/reducers' import { EditorConfigReducer } from './editor/reducers'
import { EditorConfig } from './editor/types' import { EditorConfig } from './editor/types'
import { NoteDetailsReducer } from './note-details/reducers'
import { NoteDetails } from './note-details/types'
import { UserReducer } from './user/reducers' import { UserReducer } from './user/reducers'
import { MaybeUserState } from './user/types' import { MaybeUserState } from './user/types'
@ -27,7 +27,7 @@ export interface ApplicationState {
apiUrl: ApiUrlObject; apiUrl: ApiUrlObject;
editorConfig: EditorConfig; editorConfig: EditorConfig;
darkMode: DarkModeConfig; darkMode: DarkModeConfig;
documentContent: DocumentContent; noteDetails: NoteDetails;
} }
export const allReducers: Reducer<ApplicationState> = combineReducers<ApplicationState>({ export const allReducers: Reducer<ApplicationState> = combineReducers<ApplicationState>({
@ -37,7 +37,7 @@ export const allReducers: Reducer<ApplicationState> = combineReducers<Applicatio
apiUrl: ApiUrlReducer, apiUrl: ApiUrlReducer,
editorConfig: EditorConfigReducer, editorConfig: EditorConfigReducer,
darkMode: DarkModeConfigReducer, darkMode: DarkModeConfigReducer,
documentContent: DocumentContentReducer noteDetails: NoteDetailsReducer
}) })
export const store = createStore(allReducers) export const store = createStore(allReducers)

View file

@ -0,0 +1,57 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { store } from '..'
import { Note } from '../../api/notes'
import { NoteFrontmatter } from '../../components/editor/note-frontmatter/note-frontmatter'
import { initialState } from './reducers'
import {
NoteDetailsActionType,
SetCheckboxInMarkdownContentAction,
SetNoteDetailsAction,
SetNoteDetailsFromServerAction,
SetNoteFrontmatterFromRenderingAction,
UpdateNoteTitleByFirstHeadingAction
} from './types'
export const setNoteMarkdownContent = (content: string): void => {
store.dispatch({
type: NoteDetailsActionType.SET_DOCUMENT_CONTENT,
content
} as SetNoteDetailsAction)
}
export const setNoteDataFromServer = (apiResponse: Note): void => {
store.dispatch({
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER,
note: apiResponse
} as SetNoteDetailsFromServerAction)
}
export const updateNoteTitleByFirstHeading = (firstHeading?: string): void => {
store.dispatch({
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING,
firstHeading: firstHeading ?? ''
} as UpdateNoteTitleByFirstHeadingAction)
}
export const setNoteFrontmatter = (frontmatter: NoteFrontmatter | undefined): void => {
if (!frontmatter) {
frontmatter = initialState.frontmatter
}
store.dispatch({
type: NoteDetailsActionType.SET_NOTE_FRONTMATTER,
frontmatter: frontmatter
} as SetNoteFrontmatterFromRenderingAction)
}
export const SetCheckboxInMarkdownContent = (lineInMarkdown: number, checked: boolean): void => {
store.dispatch({
type: NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT,
checked: checked,
lineInMarkdown: lineInMarkdown
} as SetCheckboxInMarkdownContentAction)
}

View file

@ -0,0 +1,127 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { DateTime } from 'luxon'
import { Reducer } from 'redux'
import { Note } from '../../api/notes'
import { NoteFrontmatter } from '../../components/editor/note-frontmatter/note-frontmatter'
import {
NoteDetails,
NoteDetailsAction,
NoteDetailsActionType,
SetCheckboxInMarkdownContentAction,
SetNoteDetailsAction,
SetNoteDetailsFromServerAction,
SetNoteFrontmatterFromRenderingAction,
UpdateNoteTitleByFirstHeadingAction
} from './types'
export const initialState: NoteDetails = {
markdownContent: '',
id: '',
createTime: DateTime.fromSeconds(0),
lastChange: {
timestamp: DateTime.fromSeconds(0),
userId: ''
},
alias: '',
preVersionTwoNote: false,
viewCount: 0,
authorship: [],
noteTitle: '',
firstHeading: '',
frontmatter: {
title: '',
description: '',
tags: [],
deprecatedTagsSyntax: false,
robots: '',
lang: 'en',
dir: 'ltr',
breaks: true,
GA: '',
disqus: '',
type: '',
opengraph: new Map<string, string>()
}
}
export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsAction> = (state: NoteDetails = initialState, action: NoteDetailsAction) => {
switch (action.type) {
case NoteDetailsActionType.SET_DOCUMENT_CONTENT:
return {
...state,
markdownContent: (action as SetNoteDetailsAction).content
}
case NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING:
return {
...state,
firstHeading: (action as UpdateNoteTitleByFirstHeadingAction).firstHeading,
noteTitle: generateNoteTitle(state.frontmatter, (action as UpdateNoteTitleByFirstHeadingAction).firstHeading)
}
case NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER:
return convertNoteToNoteDetails((action as SetNoteDetailsFromServerAction).note)
case NoteDetailsActionType.SET_NOTE_FRONTMATTER:
return {
...state,
frontmatter: (action as SetNoteFrontmatterFromRenderingAction).frontmatter,
noteTitle: generateNoteTitle((action as SetNoteFrontmatterFromRenderingAction).frontmatter, state.firstHeading)
}
case NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT:
return {
...state,
markdownContent: setCheckboxInMarkdownContent(
state.markdownContent,
(action as SetCheckboxInMarkdownContentAction).lineInMarkdown,
(action as SetCheckboxInMarkdownContentAction).checked
)
}
default:
return state
}
}
const TASK_REGEX = /(\s*[-*] )(\[[ xX]])( .*)/
const setCheckboxInMarkdownContent = (markdownContent: string, lineInMarkdown: number, checked: boolean): string => {
const lines = markdownContent.split('\n')
const results = TASK_REGEX.exec(lines[lineInMarkdown])
if (results) {
const before = results[1]
const after = results[3]
lines[lineInMarkdown] = `${before}[${checked ? 'x' : ' '}]${after}`
return lines.join('\n')
}
return markdownContent
}
const generateNoteTitle = (frontmatter: NoteFrontmatter, firstHeading?: string) => {
if (frontmatter?.title && frontmatter?.title !== '') {
return frontmatter.title.trim()
} else if (frontmatter?.opengraph && frontmatter?.opengraph.get('title') && frontmatter?.opengraph.get('title') !== '') {
return (frontmatter?.opengraph.get('title') ?? firstHeading ?? '').trim()
} else {
return (firstHeading ?? firstHeading ?? '').trim()
}
}
const convertNoteToNoteDetails = (note: Note): NoteDetails => {
return {
markdownContent: note.content,
frontmatter: initialState.frontmatter,
id: note.id,
noteTitle: initialState.noteTitle,
createTime: DateTime.fromSeconds(note.createTime),
lastChange: {
userId: note.lastChange.userId,
timestamp: DateTime.fromSeconds(note.lastChange.timestamp)
},
firstHeading: initialState.firstHeading,
preVersionTwoNote: note.preVersionTwoNote,
viewCount: note.viewCount,
alias: note.alias,
authorship: note.authorship
}
}

View file

@ -0,0 +1,67 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { DateTime } from 'luxon'
import { Action } from 'redux'
import { Note } from '../../api/notes'
import { NoteFrontmatter } from '../../components/editor/note-frontmatter/note-frontmatter'
export enum NoteDetailsActionType {
SET_DOCUMENT_CONTENT = 'note-details/set',
SET_NOTE_DATA_FROM_SERVER = 'note-details/data/server/set',
SET_NOTE_FRONTMATTER = 'note-details/frontmatter/set',
UPDATE_NOTE_TITLE_BY_FIRST_HEADING = 'note-details/update-note-title-by-first-heading',
SET_CHECKBOX_IN_MARKDOWN_CONTENT = 'note-details/toggle-checkbox-in-markdown-content'
}
interface LastChange {
userId: string
timestamp: DateTime
}
export interface NoteDetails {
markdownContent: string
id: string
createTime: DateTime
lastChange: LastChange
preVersionTwoNote: boolean
viewCount: number
alias: string
authorship: number[]
noteTitle: string
firstHeading: string
frontmatter: NoteFrontmatter
}
export interface NoteDetailsAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType
}
export interface SetNoteDetailsAction extends NoteDetailsAction {
type: NoteDetailsActionType.SET_DOCUMENT_CONTENT
content: string
}
export interface SetNoteDetailsFromServerAction extends NoteDetailsAction {
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER
note: Note
}
export interface UpdateNoteTitleByFirstHeadingAction extends NoteDetailsAction {
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING
firstHeading: string
}
export interface SetNoteFrontmatterFromRenderingAction extends NoteDetailsAction {
type: NoteDetailsActionType.SET_NOTE_FRONTMATTER
frontmatter: NoteFrontmatter
}
export interface SetCheckboxInMarkdownContentAction extends NoteDetailsAction {
type: NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT,
lineInMarkdown: number,
checked: boolean
}