mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 11:16:31 -05:00
Reorganize redux and hooks (1/4) (#985)
Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
bdf8110676
commit
1b7abf9f27
61 changed files with 898 additions and 986 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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')
|
||||||
})
|
})
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
describe('Help Dialog', () => {
|
describe('Help Dialog', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('/n/test')
|
cy.visitTestEditor()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('ToDo-List', () => {
|
it('ToDo-List', () => {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -26,3 +26,4 @@ import './config'
|
||||||
import './fill'
|
import './fill'
|
||||||
import './getMarkdownRenderer'
|
import './getMarkdownRenderer'
|
||||||
import './login'
|
import './login'
|
||||||
|
import './visit-test-editor'
|
||||||
|
|
33
cypress/support/visit-test-editor.ts
Normal file
33
cypress/support/visit-test-editor.ts
Normal 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
|
||||||
|
})
|
||||||
|
})
|
13
public/api/v2/notes/features-get
Normal file
13
public/api/v2/notes/features-get
Normal file
File diff suppressed because one or more lines are too long
|
@ -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)
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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'>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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'}>
|
||||||
|
|
|
@ -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>,
|
|
@ -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])
|
||||||
|
}
|
|
@ -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]
|
||||||
|
}
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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}/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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'>
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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\ dle|<f2> right"];
|
|
||||||
struct2 [label="<f0> one|<f1> two"];
|
|
||||||
struct3 [label="hello\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!
|
|
||||||
|
|
||||||
`
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
},
|
},
|
|
@ -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',
|
|
@ -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])
|
||||||
|
}
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
32
src/components/editor/useLoadNoteFromServer.ts
Normal file
32
src/components/editor/useLoadNoteFromServer.ts
Normal 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]
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
28
src/components/pad-view-only/ErrorWhileLoadingNoteAlert.tsx
Normal file
28
src/components/pad-view-only/ErrorWhileLoadingNoteAlert.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
24
src/components/pad-view-only/LoadingNoteAlert.tsx
Normal file
24
src/components/pad-view-only/LoadingNoteAlert.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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/>
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
18
src/hooks/common/use-document-title-with-note-title.ts
Normal file
18
src/hooks/common/use-document-title-with-note-title.ts
Normal 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)
|
||||||
|
}
|
12
src/hooks/common/use-note-markdown-content.ts
Normal file
12
src/hooks/common/use-note-markdown-content.ts
Normal 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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
57
src/redux/note-details/methods.ts
Normal file
57
src/redux/note-details/methods.ts
Normal 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)
|
||||||
|
}
|
127
src/redux/note-details/reducers.ts
Normal file
127
src/redux/note-details/reducers.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
67
src/redux/note-details/types.ts
Normal file
67
src/redux/note-details/types.ts
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue