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
|
||||
- `{%pdf https://example.org/example-pdf.pdf %}` -> Embedding removed
|
||||
- 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
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
describe('Autocompletion', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/n/test')
|
||||
cy.visitTestEditor()
|
||||
cy.get('.CodeMirror')
|
||||
.click()
|
||||
.get('textarea')
|
||||
|
|
|
@ -9,7 +9,7 @@ import { branding } from '../support/config'
|
|||
const title = 'This is a test title'
|
||||
describe('Document Title', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/n/test')
|
||||
cy.visitTestEditor()
|
||||
cy.get('.btn.active.btn-outline-secondary > i.fa-columns')
|
||||
.should('exist')
|
||||
})
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
|
||||
describe('Editor mode from URL parameter is used', () => {
|
||||
it('mode view', () => {
|
||||
cy.visit('/n/features?view')
|
||||
cy.visitTestEditor('view')
|
||||
cy.get('.splitter.left')
|
||||
.should('have.class', 'd-none')
|
||||
cy.get('.splitter.right')
|
||||
.should('not.have.class', 'd-none')
|
||||
})
|
||||
it('mode both', () => {
|
||||
cy.visit('/n/features?both')
|
||||
cy.visitTestEditor('both')
|
||||
cy.get('.splitter.left')
|
||||
.should('not.have.class', 'd-none')
|
||||
cy.get('.splitter.separator')
|
||||
|
@ -22,7 +22,7 @@ describe('Editor mode from URL parameter is used', () => {
|
|||
.should('not.have.class', 'd-none')
|
||||
})
|
||||
it('mode edit', () => {
|
||||
cy.visit('/n/features?edit')
|
||||
cy.visitTestEditor('edit')
|
||||
cy.get('.splitter.left')
|
||||
.should('not.have.class', 'd-none')
|
||||
cy.get('.splitter.right')
|
||||
|
|
|
@ -9,7 +9,7 @@ describe('Export', () => {
|
|||
const testContent = `---\ntitle: ${testTitle}\n---\nThis is some test content`
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/n/test')
|
||||
cy.visitTestEditor()
|
||||
cy.codemirrorFill(testContent)
|
||||
})
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const imageUrl = 'http://example.com/non-existing.png'
|
|||
|
||||
describe('File upload', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/n/test')
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
it('doesn\'t prevent drag\'n\'drop of plain text', () => {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
describe('Help Dialog', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/n/test')
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
it('ToDo-List', () => {
|
||||
|
|
|
@ -12,7 +12,7 @@ const findHljsCodeBlock = () => {
|
|||
|
||||
describe('Code', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/n/test')
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
describe('with just the language', () => {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
describe('Import markdown file', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/n/test')
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
it('import on blank note', () => {
|
||||
|
|
|
@ -10,7 +10,7 @@ describe('The status bar text length info', () => {
|
|||
const tooMuchTestContent = `${dangerTestContent}a`
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/n/test')
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
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'
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/n/test')
|
||||
cy.visitTestEditor()
|
||||
|
||||
cy.get('.CodeMirror')
|
||||
.click()
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
describe('YAML Array for deprecated syntax of document tags in frontmatter', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/n/features')
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
it('is shown when using old syntax', () => {
|
||||
|
|
|
@ -26,3 +26,4 @@ import './config'
|
|||
import './fill'
|
||||
import './getMarkdownRenderer'
|
||||
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
|
||||
alias: string
|
||||
lastChange: LastChange
|
||||
viewcount: number
|
||||
createtime: number
|
||||
viewCount: number
|
||||
createTime: number
|
||||
content: string
|
||||
authorship: number[]
|
||||
preVersionTwoNote: boolean
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
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-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Alert, Button, Col, ListGroup, Modal, Row } from 'react-bootstrap'
|
||||
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useParams } from 'react-router'
|
||||
import { getAllRevisions, getRevision } from '../../../../api/revisions'
|
||||
import { Revision, RevisionListEntry } from '../../../../api/revisions/types'
|
||||
import { UserResponse } from '../../../../api/users/types'
|
||||
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 { ShowIf } from '../../../common/show-if/show-if'
|
||||
import { RevisionModalListEntry } from './revision-modal-list-entry'
|
||||
|
@ -58,7 +57,7 @@ export const RevisionModal: React.FC<PermissionsModalProps> = ({ show, onHide })
|
|||
}).catch(() => setError(true))
|
||||
}, [selectedRevisionTimestamp, id])
|
||||
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
const markdownContent = useNoteMarkdownContent()
|
||||
|
||||
return (
|
||||
<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 React from 'react'
|
||||
import { Modal } from 'react-bootstrap'
|
||||
import { useTranslation , Trans } from 'react-i18next'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url'
|
||||
|
@ -24,7 +24,7 @@ export interface ShareModalProps {
|
|||
|
||||
export const ShareModal: React.FC<ShareModalProps> = ({ show, onHide }) => {
|
||||
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 baseUrl = useFrontendBaseUrl()
|
||||
const { id } = useParams<EditorPathParams>()
|
||||
|
@ -39,11 +39,11 @@ export const ShareModal: React.FC<ShareModalProps> = ({ show, onHide }) => {
|
|||
<Trans i18nKey={'editor.modal.shareLink.editorDescription'}/>
|
||||
<CopyableField content={`${baseUrl}/n/${id}?${editorMode}`} nativeShareButton={true}
|
||||
url={`${baseUrl}/n/${id}?${editorMode}`}/>
|
||||
<ShowIf condition={noteMetadata.type === 'slide'}>
|
||||
<ShowIf condition={noteFrontmatter.type === 'slide'}>
|
||||
<Trans i18nKey={'editor.modal.shareLink.slidesDescription'}/>
|
||||
<CopyableField content={`${baseUrl}/p/${id}`} nativeShareButton={true} url={`${baseUrl}/p/${id}`}/>
|
||||
</ShowIf>
|
||||
<ShowIf condition={noteMetadata.type === ''}>
|
||||
<ShowIf condition={noteFrontmatter.type === ''}>
|
||||
<Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'}/>
|
||||
<CopyableField content={`${baseUrl}/s/${id}`} nativeShareButton={true} url={`${baseUrl}/s/${id}`}/>
|
||||
</ShowIf>
|
||||
|
|
|
@ -3,21 +3,24 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import equal from 'fast-deep-equal'
|
||||
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated'
|
||||
import { ApplicationState } from '../../../redux'
|
||||
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 { 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,
|
||||
onTaskCheckedChange,
|
||||
onMetadataChange,
|
||||
onFrontmatterChange,
|
||||
scrollState,
|
||||
onFirstHeadingChange,
|
||||
wide,
|
||||
|
@ -25,40 +28,40 @@ export const DocumentIframe: React.FC<ScrollingDocumentRenderPaneProps> = (
|
|||
onMakeScrollSource,
|
||||
extraClasses
|
||||
}) => {
|
||||
const frameReference = useRef<HTMLIFrameElement>(null)
|
||||
const darkMode = useIsDarkModeActivated()
|
||||
const [rendererReady, setRendererReady] = useState<boolean>(false)
|
||||
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
|
||||
|
||||
const frameReference = useRef<HTMLIFrameElement>(null)
|
||||
const rendererOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.rendererOrigin)
|
||||
const renderPageUrl = `${rendererOrigin}/render`
|
||||
const resetRendererReady = useCallback(() => setRendererReady(false), [])
|
||||
const iframeCommunicator = useMemo(() => new IframeEditorToRendererCommunicator(), [])
|
||||
const onIframeLoad = useOnIframeLoad(frameReference, iframeCommunicator, rendererOrigin, renderPageUrl, resetRendererReady)
|
||||
|
||||
useEffect(() => () => iframeCommunicator.unregisterEventListener(), [iframeCommunicator])
|
||||
|
||||
const [rendererReady, setRendererReady] = useState<boolean>(false)
|
||||
|
||||
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.onSetScrollSourceToRenderer(onMakeScrollSource), [iframeCommunicator, onMakeScrollSource])
|
||||
useEffect(() => iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange), [iframeCommunicator, onTaskCheckedChange])
|
||||
useEffect(() => iframeCommunicator.onImageClicked(setLightboxDetails), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator.onRendererReady(() => setRendererReady(true)), [darkMode, iframeCommunicator, scrollState, wide])
|
||||
|
||||
useEffect(() => {
|
||||
if (rendererReady) {
|
||||
iframeCommunicator.sendSetMarkdownContent(markdownContent)
|
||||
}
|
||||
}, [iframeCommunicator, markdownContent, rendererReady])
|
||||
useEffect(() => {
|
||||
if (rendererReady) {
|
||||
iframeCommunicator.sendSetDarkmode(darkMode)
|
||||
}
|
||||
}, [darkMode, iframeCommunicator, rendererReady])
|
||||
|
||||
const oldScrollState = useRef<ScrollState | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
if (rendererReady) {
|
||||
if (rendererReady && !equal(scrollState, oldScrollState.current)) {
|
||||
oldScrollState.current = scrollState
|
||||
iframeCommunicator.sendScrollState(scrollState)
|
||||
}
|
||||
}, [iframeCommunicator, rendererReady, scrollState])
|
||||
|
||||
useEffect(() => {
|
||||
if (rendererReady) {
|
||||
iframeCommunicator.sendSetWide(wide ?? false)
|
||||
|
@ -69,37 +72,17 @@ export const DocumentIframe: React.FC<ScrollingDocumentRenderPaneProps> = (
|
|||
if (rendererReady) {
|
||||
iframeCommunicator.sendSetBaseUrl(window.location.toString())
|
||||
}
|
||||
}, [iframeCommunicator, rendererReady,])
|
||||
}, [iframeCommunicator, rendererReady])
|
||||
|
||||
const sendToRenderPage = useRef<boolean>(true)
|
||||
|
||||
const onLoad = useCallback(() => {
|
||||
const frame = frameReference.current
|
||||
if (!frame || !frame.contentWindow) {
|
||||
iframeCommunicator.unsetOtherSide()
|
||||
return
|
||||
useEffect(() => {
|
||||
if (rendererReady) {
|
||||
iframeCommunicator.sendSetMarkdownContent(markdownContent)
|
||||
}
|
||||
|
||||
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)
|
||||
}, [])
|
||||
}, [iframeCommunicator, markdownContent, rendererReady])
|
||||
|
||||
return <Fragment>
|
||||
<ImageLightboxModal show={!!lightboxDetails} onHide={hideLightbox} src={lightboxDetails?.src}
|
||||
alt={lightboxDetails?.alt} title={lightboxDetails?.title}/>
|
||||
<iframe data-cy={'documentIframe'} onLoad={onLoad} title="render" src={renderPageUrl}
|
||||
<ShowOnPropChangeImageLightbox details={lightboxDetails}/>
|
||||
<iframe data-cy={'documentIframe'} onLoad={onIframeLoad} title="render" src={renderPageUrl}
|
||||
{...isTestMode() ? {} : { sandbox: 'allow-downloads allow-same-origin allow-scripts allow-popups' }}
|
||||
ref={frameReference} className={`h-100 w-100 border-0 ${extraClasses ?? ''}`}/>
|
||||
</Fragment>
|
||||
|
|
|
@ -5,26 +5,23 @@
|
|||
*/
|
||||
|
||||
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 useResizeObserver from 'use-resize-observer'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { FullMarkdownRenderer } from '../../markdown-renderer/full-markdown-renderer'
|
||||
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 { YAMLMetaData } from '../yaml-metadata/yaml-metadata'
|
||||
import { useAdaptedLineMarkerCallback } from './use-adapted-line-markers-callback'
|
||||
import { useSyncedScrolling } from './hooks/use-synced-scrolling'
|
||||
import { YamlArrayDeprecationAlert } from './yaml-array-deprecation-alert'
|
||||
|
||||
export interface DocumentRenderPaneProps {
|
||||
export interface DocumentRenderPaneProps extends ScrollProps {
|
||||
extraClasses?: string
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||
onMetadataChange?: (metaData: YAMLMetaData | undefined) => void
|
||||
onMouseEnterRenderer?: () => void
|
||||
onScrollRenderer?: () => void
|
||||
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
|
||||
wide?: boolean,
|
||||
|
@ -37,33 +34,27 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
|||
{
|
||||
extraClasses,
|
||||
onFirstHeadingChange,
|
||||
onLineMarkerPositionChanged,
|
||||
onMetadataChange,
|
||||
onMouseEnterRenderer,
|
||||
onScrollRenderer,
|
||||
onFrontmatterChange,
|
||||
onMakeScrollSource,
|
||||
onTaskCheckedChange,
|
||||
documentRenderPaneRef,
|
||||
wide,
|
||||
baseUrl,
|
||||
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 changeLineMarker = useAdaptedLineMarkerCallback(documentRenderPaneRef, rendererRef, onLineMarkerPositionChanged)
|
||||
const setContainerReference = useCallback((instance: HTMLDivElement | null) => {
|
||||
if (documentRenderPaneRef) {
|
||||
documentRenderPaneRef.current = instance || null
|
||||
}
|
||||
internalDocumentRenderPaneRef.current = instance || undefined
|
||||
}, [documentRenderPaneRef])
|
||||
const internalDocumentRenderPaneRef = useRef<HTMLDivElement>(null)
|
||||
const [tocAst, setTocAst] = useState<TocAst>()
|
||||
const width = useResizeObserver({ ref: internalDocumentRenderPaneRef.current }).width ?? 0
|
||||
|
||||
const contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent])
|
||||
const [onLineMarkerPositionChanged, onUserScroll] = useSyncedScrolling(internalDocumentRenderPaneRef, rendererRef, contentLineCount, scrollState, onScroll)
|
||||
|
||||
return (
|
||||
<div className={`bg-light m-0 pb-5 row ${extraClasses ?? ''}`}
|
||||
ref={setContainerReference} onScroll={onScrollRenderer} onMouseEnter={onMouseEnterRenderer}>
|
||||
<div className={`overflow-y-scroll h-100 bg-light m-0 pb-5 row ${extraClasses ?? ''}`}
|
||||
ref={internalDocumentRenderPaneRef} onScroll={onUserScroll} onMouseEnter={onMakeScrollSource}>
|
||||
<div className={'col-md d-none d-md-block'}/>
|
||||
<div className={'bg-light col'}>
|
||||
<YamlArrayDeprecationAlert/>
|
||||
|
@ -73,8 +64,8 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
|||
className={'flex-fill pt-4 mb-3'}
|
||||
content={markdownContent}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onLineMarkerPositionChanged={changeLineMarker}
|
||||
onMetaDataChange={onMetadataChange}
|
||||
onLineMarkerPositionChanged={onLineMarkerPositionChanged}
|
||||
onFrontmatterChange={onFrontmatterChange}
|
||||
onTaskCheckedChange={onTaskCheckedChange}
|
||||
onTocChange={(tocAst) => setTocAst(tocAst)}
|
||||
wide={wide}
|
||||
|
@ -85,10 +76,10 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
|||
|
||||
<div className={'col-md pt-4'}>
|
||||
<ShowIf condition={!!tocAst}>
|
||||
<ShowIf condition={realWidth >= 1280}>
|
||||
<ShowIf condition={width >= 1280}>
|
||||
<TableOfContents ast={tocAst as TocAst} className={'sticky'} baseUrl={baseUrl}/>
|
||||
</ShowIf>
|
||||
<ShowIf condition={realWidth < 1280}>
|
||||
<ShowIf condition={width < 1280}>
|
||||
<div className={'markdown-toc-sidebar-button'}>
|
||||
<Dropdown drop={'up'}>
|
||||
<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-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { RefObject, useCallback } from 'react'
|
||||
import { LineMarkerPosition } from '../../markdown-renderer/types'
|
||||
import { LineMarkerPosition } from '../../../markdown-renderer/types'
|
||||
|
||||
export const useAdaptedLineMarkerCallback = (documentRenderPaneRef: RefObject<HTMLDivElement> | undefined,
|
||||
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 = () => {
|
||||
useTranslation()
|
||||
const yamlDeprecatedTags = useSelector((state: ApplicationState) => state.documentContent.metadata.deprecatedTagsSyntax)
|
||||
const yamlDeprecatedTags = useSelector((state: ApplicationState) => state.noteDetails.frontmatter.deprecatedTagsSyntax)
|
||||
|
||||
return <ShowIf condition={yamlDeprecatedTags}>
|
||||
<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 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')
|
||||
uploadFile(noteId, mimeType, file)
|
||||
.then(({ link }) => {
|
||||
|
|
|
@ -7,25 +7,32 @@
|
|||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useLocation, useParams } from 'react-router'
|
||||
import { useLocation } from 'react-router'
|
||||
import useMedia from 'use-media'
|
||||
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 { setDocumentContent, setDocumentMetadata, setNoteId } from '../../redux/document-content/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 { 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 { EditorMode } from './app-bar/editor-view-mode'
|
||||
import { DocumentIframe } from './document-renderer-pane/document-iframe'
|
||||
import { EditorPane } from './editor-pane/editor-pane'
|
||||
import { editorTestContent } from './editorTestContent'
|
||||
import { useViewModeShortcuts } from './hooks/useViewModeShortcuts'
|
||||
import { DualScrollState, ScrollState } from './scroll/scroll-props'
|
||||
import { Sidebar } from './sidebar/sidebar'
|
||||
import { Splitter } from './splitter/splitter'
|
||||
import { YAMLMetaData } from './yaml-metadata/yaml-metadata'
|
||||
import { useLoadNoteFromServer } from './useLoadNoteFromServer'
|
||||
|
||||
export interface EditorPathParams {
|
||||
id: string
|
||||
|
@ -36,18 +43,11 @@ export enum ScrollSource {
|
|||
RENDERER
|
||||
}
|
||||
|
||||
const TASK_REGEX = /(\s*[-*] )(\[[ xX]])( .*)/
|
||||
|
||||
export const Editor: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams<EditorPathParams>()
|
||||
useTranslation()
|
||||
const { search } = useLocation()
|
||||
const untitledNote = t('editor.untitledNote')
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
const markdownContent = useNoteMarkdownContent()
|
||||
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 editorMode: EditorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
|
||||
|
@ -59,7 +59,6 @@ export const Editor: React.FC = () => {
|
|||
}))
|
||||
|
||||
useEffect(() => {
|
||||
setDocumentContent(editorTestContent)
|
||||
const requestedMode = search.substr(1)
|
||||
const mode = Object.values(EditorMode).find(mode => mode === requestedMode)
|
||||
if (mode) {
|
||||
|
@ -67,39 +66,6 @@ export const Editor: React.FC = () => {
|
|||
}
|
||||
}, [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(() => {
|
||||
if (!isWide && editorMode === EditorMode.BOTH) {
|
||||
setEditorMode(EditorMode.PREVIEW)
|
||||
|
@ -118,8 +84,10 @@ export const Editor: React.FC = () => {
|
|||
}
|
||||
}, [editorSyncScroll])
|
||||
|
||||
useViewModeShortcuts()
|
||||
useApplyDarkMode()
|
||||
useDocumentTitle(documentTitle)
|
||||
useDocumentTitleWithNoteTitle()
|
||||
const [error, loading] = useLoadNoteFromServer()
|
||||
|
||||
const setRendererToScrollSource = useCallback(() => {
|
||||
scrollSource.current = ScrollSource.RENDERER
|
||||
|
@ -134,32 +102,39 @@ export const Editor: React.FC = () => {
|
|||
<MotdBanner/>
|
||||
<div className={'d-flex flex-column vh-100'}>
|
||||
<AppBar mode={AppBarMode.EDITOR}/>
|
||||
<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={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 className={'container'}>
|
||||
<ErrorWhileLoadingNoteAlert show={error}/>
|
||||
<LoadingNoteAlert show={loading}/>
|
||||
</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>
|
||||
</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 MarkdownIt from 'markdown-it'
|
||||
import frontmatter from 'markdown-it-front-matter'
|
||||
import { RawYAMLMetadata, YAMLMetaData } from './yaml-metadata'
|
||||
import { NoteFrontmatter, RawNoteFrontmatter } from './note-frontmatter'
|
||||
|
||||
describe('yaml tests', () => {
|
||||
let raw: RawYAMLMetadata | undefined
|
||||
let finished: YAMLMetaData | undefined
|
||||
describe('yaml frontmatter tests', () => {
|
||||
let raw: RawNoteFrontmatter | undefined
|
||||
let finished: NoteFrontmatter | undefined
|
||||
const md = new MarkdownIt('default', {
|
||||
html: true,
|
||||
breaks: true,
|
||||
|
@ -19,15 +19,15 @@ describe('yaml tests', () => {
|
|||
typographer: true
|
||||
})
|
||||
md.use(frontmatter, (rawMeta: string) => {
|
||||
raw = yaml.load(rawMeta) as RawYAMLMetadata
|
||||
finished = new YAMLMetaData(raw)
|
||||
raw = yaml.load(rawMeta) as RawNoteFrontmatter
|
||||
finished = new NoteFrontmatter(raw)
|
||||
})
|
||||
|
||||
// generate default YAMLMetadata
|
||||
md.render('---\n---')
|
||||
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)
|
||||
expect(raw).not.toBe(undefined)
|
||||
expect(raw).toEqual(expectedRaw)
|
||||
|
@ -44,47 +44,47 @@ describe('yaml tests', () => {
|
|||
})
|
||||
|
||||
it('title only', () => {
|
||||
testMetadata(`---
|
||||
testFrontmatter(`---
|
||||
title: test
|
||||
___
|
||||
`,
|
||||
{
|
||||
title: 'test'
|
||||
},
|
||||
{
|
||||
title: 'test'
|
||||
})
|
||||
{
|
||||
title: 'test'
|
||||
},
|
||||
{
|
||||
title: 'test'
|
||||
})
|
||||
})
|
||||
|
||||
it('robots only', () => {
|
||||
testMetadata(`---
|
||||
testFrontmatter(`---
|
||||
robots: index, follow
|
||||
___
|
||||
`,
|
||||
{
|
||||
robots: 'index, follow'
|
||||
},
|
||||
{
|
||||
robots: 'index, follow'
|
||||
})
|
||||
{
|
||||
robots: 'index, follow'
|
||||
},
|
||||
{
|
||||
robots: 'index, follow'
|
||||
})
|
||||
})
|
||||
|
||||
it('tags only (old syntax)', () => {
|
||||
testMetadata(`---
|
||||
testFrontmatter(`---
|
||||
tags: test123, abc
|
||||
___
|
||||
`,
|
||||
{
|
||||
tags: 'test123, abc'
|
||||
},
|
||||
{
|
||||
tags: ['test123', 'abc'],
|
||||
deprecatedTagsSyntax: true
|
||||
})
|
||||
{
|
||||
tags: 'test123, abc'
|
||||
},
|
||||
{
|
||||
tags: ['test123', 'abc'],
|
||||
deprecatedTagsSyntax: true
|
||||
})
|
||||
})
|
||||
|
||||
it('tags only', () => {
|
||||
testMetadata(`---
|
||||
testFrontmatter(`---
|
||||
tags:
|
||||
- test123
|
||||
- abc
|
||||
|
@ -100,7 +100,7 @@ describe('yaml tests', () => {
|
|||
})
|
||||
|
||||
it('tags only (alternative syntax)', () => {
|
||||
testMetadata(`---
|
||||
testFrontmatter(`---
|
||||
tags: ['test123', 'abc']
|
||||
___
|
||||
`,
|
||||
|
@ -114,21 +114,21 @@ describe('yaml tests', () => {
|
|||
})
|
||||
|
||||
it('breaks only', () => {
|
||||
testMetadata(`---
|
||||
testFrontmatter(`---
|
||||
breaks: false
|
||||
___
|
||||
`,
|
||||
{
|
||||
breaks: false
|
||||
},
|
||||
{
|
||||
breaks: false
|
||||
})
|
||||
{
|
||||
breaks: false
|
||||
},
|
||||
{
|
||||
breaks: false
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
it('slideOptions nothing', () => {
|
||||
testMetadata(`---
|
||||
testFrontmatter(`---
|
||||
slideOptions:
|
||||
___
|
||||
`,
|
||||
|
@ -144,7 +144,7 @@ describe('yaml tests', () => {
|
|||
})
|
||||
|
||||
it('slideOptions.theme only', () => {
|
||||
testMetadata(`---
|
||||
testFrontmatter(`---
|
||||
slideOptions:
|
||||
theme: sky
|
||||
___
|
||||
|
@ -164,7 +164,7 @@ describe('yaml tests', () => {
|
|||
})
|
||||
|
||||
it('slideOptions full', () => {
|
||||
testMetadata(`---
|
||||
testFrontmatter(`---
|
||||
slideOptions:
|
||||
transition: zoom
|
||||
theme: sky
|
||||
|
@ -186,46 +186,46 @@ describe('yaml tests', () => {
|
|||
*/
|
||||
|
||||
it('opengraph nothing', () => {
|
||||
testMetadata(`---
|
||||
testFrontmatter(`---
|
||||
opengraph:
|
||||
___
|
||||
`,
|
||||
{
|
||||
opengraph: null
|
||||
},
|
||||
{
|
||||
opengraph: new Map<string, string>()
|
||||
})
|
||||
{
|
||||
opengraph: null
|
||||
},
|
||||
{
|
||||
opengraph: new Map<string, string>()
|
||||
})
|
||||
})
|
||||
|
||||
it('opengraph title only', () => {
|
||||
testMetadata(`---
|
||||
testFrontmatter(`---
|
||||
opengraph:
|
||||
title: Testtitle
|
||||
___
|
||||
`,
|
||||
{
|
||||
opengraph: {
|
||||
title: 'Testtitle'
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
opengraph: {
|
||||
title: 'Testtitle'
|
||||
}
|
||||
},
|
||||
{
|
||||
opengraph: new Map<string, string>(Object.entries({ title: 'Testtitle' }))
|
||||
})
|
||||
})
|
||||
|
||||
it('opengraph more attributes', () => {
|
||||
testMetadata(`---
|
||||
testFrontmatter(`---
|
||||
opengraph:
|
||||
title: Testtitle
|
||||
image: https://dummyimage.com/48.png
|
||||
image:type: image/png
|
||||
___
|
||||
`,
|
||||
{
|
||||
opengraph: {
|
||||
title: 'Testtitle',
|
||||
image: 'https://dummyimage.com/48.png',
|
||||
{
|
||||
opengraph: {
|
||||
title: 'Testtitle',
|
||||
image: 'https://dummyimage.com/48.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'
|
||||
|
||||
export interface RawYAMLMetadata {
|
||||
export interface RawNoteFrontmatter {
|
||||
title: string | undefined
|
||||
description: string | undefined
|
||||
tags: string | string[] | undefined
|
||||
|
@ -23,7 +23,7 @@ export interface RawYAMLMetadata {
|
|||
opengraph: { [key: string]:string } | null
|
||||
}
|
||||
|
||||
export class YAMLMetaData {
|
||||
export class NoteFrontmatter {
|
||||
title: string
|
||||
description: string
|
||||
tags: string[]
|
||||
|
@ -38,7 +38,7 @@ export class YAMLMetaData {
|
|||
// slideOptions: RevealOptions
|
||||
opengraph: Map<string, string>
|
||||
|
||||
constructor (rawData: RawYAMLMetadata) {
|
||||
constructor (rawData: RawNoteFrontmatter) {
|
||||
this.title = rawData?.title ?? ''
|
||||
this.description = rawData?.description ?? ''
|
||||
this.robots = rawData?.robots ?? ''
|
||||
|
@ -46,9 +46,9 @@ export class YAMLMetaData {
|
|||
this.GA = rawData?.GA ?? ''
|
||||
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.dir = (rawData?.dir as YAMLMetaData['dir']) ?? 'ltr'
|
||||
this.dir = (rawData?.dir as NoteFrontmatter['dir']) ?? 'ltr'
|
||||
|
||||
/* this.slideOptions = (rawData?.slideOptions as RevealOptions) ?? {
|
||||
transition: 'none',
|
|
@ -8,13 +8,13 @@ import { RefObject, useCallback } from 'react'
|
|||
import { LineMarkerPosition } from '../../../markdown-renderer/types'
|
||||
import { ScrollState } from '../scroll-props'
|
||||
|
||||
export const useOnUserScroll = (lineMarks: LineMarkerPosition[] | undefined, renderer: RefObject<HTMLElement>, onScroll: ((newScrollState: ScrollState) => void) | undefined): () => void =>
|
||||
useCallback(() => {
|
||||
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !onScroll) {
|
||||
export const useOnUserScroll = (lineMarks: LineMarkerPosition[] | undefined, scrollContainer: RefObject<HTMLElement>, onScroll: ((newScrollState: ScrollState) => void) | undefined): () => void => {
|
||||
return useCallback(() => {
|
||||
if (!scrollContainer.current || !lineMarks || lineMarks.length === 0 || !onScroll) {
|
||||
return
|
||||
}
|
||||
|
||||
const scrollTop = renderer.current.scrollTop
|
||||
const scrollTop = scrollContainer.current.scrollTop
|
||||
|
||||
const lineMarksBeforeScrollTop = lineMarks.filter(lineMark => lineMark.position <= scrollTop)
|
||||
if (lineMarksBeforeScrollTop.length === 0) {
|
||||
|
@ -44,4 +44,5 @@ export const useOnUserScroll = (lineMarks: LineMarkerPosition[] | undefined, ren
|
|||
|
||||
const newScrollState: ScrollState = { firstLineInView: line, scrolledPercentage: innerScrolling }
|
||||
onScroll(newScrollState)
|
||||
}, [lineMarks, onScroll, renderer])
|
||||
}, [lineMarks, onScroll, scrollContainer])
|
||||
}
|
||||
|
|
|
@ -9,21 +9,21 @@ import { LineMarkerPosition } from '../../../markdown-renderer/types'
|
|||
import { ScrollState } from '../scroll-props'
|
||||
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 scrollTo = useCallback((targetPosition: number): void => {
|
||||
if (!renderer.current || targetPosition === lastScrollPosition.current) {
|
||||
if (!scrollContainer.current || targetPosition === lastScrollPosition.current) {
|
||||
return
|
||||
}
|
||||
lastScrollPosition.current = targetPosition
|
||||
renderer.current.scrollTo({
|
||||
scrollContainer.current.scrollTo({
|
||||
top: targetPosition
|
||||
})
|
||||
}, [renderer])
|
||||
}, [scrollContainer])
|
||||
|
||||
useEffect(() => {
|
||||
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !scrollState) {
|
||||
if (!scrollContainer.current || !lineMarks || lineMarks.length === 0 || !scrollState) {
|
||||
return
|
||||
}
|
||||
if (scrollState.firstLineInView < lineMarks[0].line) {
|
||||
|
@ -31,12 +31,12 @@ export const useScrollToLineMark = (scrollState: ScrollState | undefined, lineMa
|
|||
return
|
||||
}
|
||||
if (scrollState.firstLineInView > lineMarks[lineMarks.length - 1].line) {
|
||||
scrollTo(renderer.current.offsetHeight)
|
||||
scrollTo(scrollContainer.current.offsetHeight)
|
||||
return
|
||||
}
|
||||
const { lastMarkBefore, firstMarkAfter } = findLineMarks(lineMarks, scrollState.firstLineInView)
|
||||
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 firstMarkAfterLine = firstMarkAfter ? firstMarkAfter.line : contentLineCount
|
||||
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 correctedPosition = Math.floor(position)
|
||||
scrollTo(correctedPosition)
|
||||
}, [contentLineCount, lineMarks, renderer, scrollState, scrollTo])
|
||||
}, [contentLineCount, lineMarks, scrollContainer, scrollState, scrollTo])
|
||||
}
|
||||
|
|
|
@ -6,16 +6,14 @@
|
|||
|
||||
import React, { useCallback } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../../redux'
|
||||
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
|
||||
import { download } from '../../common/download/download'
|
||||
import { SidebarButton } from './sidebar-button'
|
||||
|
||||
export const ExportMarkdownSidebarEntry: React.FC = () => {
|
||||
useTranslation()
|
||||
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
const markdownContent = useNoteMarkdownContent()
|
||||
const onClick = useCallback(() => {
|
||||
download(markdownContent, `title.md`, 'text/markdown') //todo: replace hard coded title with redux
|
||||
}, [markdownContent])
|
||||
|
|
|
@ -6,14 +6,13 @@
|
|||
|
||||
import React, { Fragment, useCallback, useRef } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../../redux'
|
||||
import { setDocumentContent } from '../../../redux/document-content/methods'
|
||||
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
|
||||
import { setNoteMarkdownContent } from '../../../redux/note-details/methods'
|
||||
import { SidebarButton } from './sidebar-button'
|
||||
import { UploadInput } from './upload-input'
|
||||
|
||||
export const ImportMarkdownSidebarEntry: React.FC = () => {
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
const markdownContent = useNoteMarkdownContent()
|
||||
useTranslation()
|
||||
|
||||
const onImportMarkdown = useCallback((file: File) => {
|
||||
|
@ -21,11 +20,7 @@ export const ImportMarkdownSidebarEntry: React.FC = () => {
|
|||
const fileReader = new FileReader()
|
||||
fileReader.addEventListener('load', () => {
|
||||
const newContent = fileReader.result as string
|
||||
if (markdownContent.length === 0) {
|
||||
setDocumentContent(newContent)
|
||||
} else {
|
||||
setDocumentContent(markdownContent + '\n' + newContent)
|
||||
}
|
||||
setNoteMarkdownContent(markdownContent.length === 0 ? newContent : `${markdownContent}\n${newContent}`)
|
||||
})
|
||||
fileReader.addEventListener('loadend', () => {
|
||||
resolve()
|
||||
|
@ -39,15 +34,16 @@ export const ImportMarkdownSidebarEntry: React.FC = () => {
|
|||
|
||||
const clickRef = useRef<(() => void)>()
|
||||
const buttonClick = useCallback(() => {
|
||||
clickRef.current?.();
|
||||
},[]);
|
||||
clickRef.current?.()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SidebarButton data-cy={"menu-import-markdown"} icon={"file-text-o"} onClick={buttonClick}>
|
||||
<Trans i18nKey={'editor.import.file'}/>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
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 { InternalLink } from '../common/links/internal-link'
|
||||
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 { 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 { useReplacerInstanceListCreator } from './hooks/use-replacer-instance-list-creator'
|
||||
import { FullMarkdownItConfigurator } from './markdown-it-configurator/FullMarkdownItConfigurator'
|
||||
|
@ -25,7 +25,7 @@ import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-po
|
|||
export interface FullMarkdownRendererProps {
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
|
||||
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
onTocChange?: (ast: TocAst) => void
|
||||
rendererRef?: Ref<HTMLDivElement>
|
||||
|
@ -37,7 +37,7 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
|
|||
{
|
||||
onFirstHeadingChange,
|
||||
onLineMarkerPositionChanged,
|
||||
onMetaDataChange,
|
||||
onFrontmatterChange,
|
||||
onTaskCheckedChange,
|
||||
onTocChange,
|
||||
content,
|
||||
|
@ -53,11 +53,11 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
|
|||
const [showYamlError, setShowYamlError] = useState(false)
|
||||
const hasNewYamlError = useRef(false)
|
||||
|
||||
const rawMetaRef = useRef<RawYAMLMetadata>()
|
||||
const rawMetaRef = useRef<RawNoteFrontmatter>()
|
||||
const firstHeadingRef = useRef<string>()
|
||||
const documentElement = useRef<HTMLDivElement>(null)
|
||||
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)
|
||||
useExtractFirstHeadline(documentElement, content, onFirstHeadingChange)
|
||||
|
||||
|
@ -66,7 +66,7 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
|
|||
|
||||
const markdownIt = useMemo(() => {
|
||||
return (new FullMarkdownItConfigurator(
|
||||
!!onMetaDataChange,
|
||||
!!onFrontmatterChange,
|
||||
errorState => hasNewYamlError.current = errorState,
|
||||
rawMeta => {
|
||||
rawMetaRef.current = rawMeta
|
||||
|
@ -78,9 +78,9 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
|
|||
currentLineMarkers.current = lineMarkers
|
||||
}
|
||||
)).buildConfiguredMarkdownIt()
|
||||
}, [onMetaDataChange])
|
||||
}, [onFrontmatterChange])
|
||||
|
||||
const clearMetadata = useCallback(() => {
|
||||
const clearFrontmatter = useCallback(() => {
|
||||
hasNewYamlError.current = false
|
||||
rawMetaRef.current = undefined
|
||||
}, [])
|
||||
|
@ -107,7 +107,7 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
|
|||
componentReplacers={allReplacers}
|
||||
markdownIt={markdownIt}
|
||||
documentReference={documentElement}
|
||||
onBeforeRendering={clearMetadata}
|
||||
onBeforeRendering={clearFrontmatter}
|
||||
onAfterRendering={checkYamlErrorState}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -4,10 +4,14 @@
|
|||
* 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 => {
|
||||
const extractInnerText = useCallback((node: ChildNode): string => {
|
||||
const extractInnerText = useCallback((node: ChildNode | null): string => {
|
||||
if (!node) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let innerText = ''
|
||||
|
||||
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) {
|
||||
node.childNodes.forEach((child) => { innerText += extractInnerText(child) })
|
||||
node.childNodes.forEach((child) => {
|
||||
innerText += extractInnerText(child)
|
||||
})
|
||||
} else if (node.nodeName === 'IMG') {
|
||||
innerText += (node as HTMLImageElement).getAttribute('alt')
|
||||
} else {
|
||||
|
@ -24,14 +30,17 @@ export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDiv
|
|||
return innerText
|
||||
}, [])
|
||||
|
||||
const lastFirstHeading = useRef<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
if (onFirstHeadingChange && documentElement.current) {
|
||||
const firstHeading = documentElement.current.getElementsByTagName('h1').item(0)
|
||||
if (firstHeading) {
|
||||
onFirstHeadingChange(extractInnerText(firstHeading))
|
||||
} else {
|
||||
onFirstHeadingChange(undefined)
|
||||
const headingText = extractInnerText(firstHeading)
|
||||
if (headingText === lastFirstHeading.current) {
|
||||
return
|
||||
}
|
||||
lastFirstHeading.current = headingText
|
||||
onFirstHeadingChange(headingText)
|
||||
}
|
||||
}, [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 { 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 { frontmatterExtract } from '../markdown-it-plugins/frontmatter'
|
||||
import { headlineAnchors } from '../markdown-it-plugins/headline-anchors'
|
||||
|
@ -28,7 +28,7 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
|
|||
constructor (
|
||||
private useFrontmatter: boolean,
|
||||
private passYamlErrorState: (error: boolean) => void,
|
||||
private onRawMeta: (rawMeta: RawYAMLMetadata) => void,
|
||||
private onRawMeta: (rawMeta: RawNoteFrontmatter) => void,
|
||||
private onToc: (toc: TocAst) => void,
|
||||
private onLineMarkers: (lineMarkers: LineMarkers[]) => void
|
||||
) {
|
||||
|
@ -45,8 +45,8 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
|
|||
!this.useFrontmatter
|
||||
? undefined
|
||||
: {
|
||||
onYamlError: (hasError: boolean) => this.passYamlErrorState(hasError),
|
||||
onRawMeta: (rawMeta: RawYAMLMetadata) => this.onRawMeta(rawMeta)
|
||||
onParseError: (hasError: boolean) => this.passYamlErrorState(hasError),
|
||||
onRawMeta: (rawMeta: RawNoteFrontmatter) => this.onRawMeta(rawMeta)
|
||||
})
|
||||
},
|
||||
headlineAnchors,
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
import yaml from 'js-yaml'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
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 {
|
||||
onYamlError: (error: boolean) => void,
|
||||
onRawMeta: (rawMeta: RawYAMLMetadata) => void,
|
||||
onParseError: (error: boolean) => void,
|
||||
onRawMeta: (rawMeta: RawNoteFrontmatter) => void,
|
||||
}
|
||||
|
||||
export const frontmatterExtract: MarkdownIt.PluginWithOptions<FrontmatterPluginOptions> = (markdownIt: MarkdownIt, options) => {
|
||||
|
@ -20,13 +20,13 @@ export const frontmatterExtract: MarkdownIt.PluginWithOptions<FrontmatterPluginO
|
|||
}
|
||||
frontmatter(markdownIt, (rawMeta: string) => {
|
||||
try {
|
||||
const meta: RawYAMLMetadata = yaml.load(rawMeta) as RawYAMLMetadata
|
||||
options.onYamlError(false)
|
||||
const meta: RawNoteFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter
|
||||
options.onParseError(false)
|
||||
options.onRawMeta(meta)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
options.onYamlError(true)
|
||||
options.onRawMeta({} as RawYAMLMetadata)
|
||||
options.onParseError(true)
|
||||
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-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { DateTime } from 'luxon'
|
||||
import React from 'react'
|
||||
|
@ -17,9 +17,9 @@ import './document-infobar.scss'
|
|||
|
||||
export interface DocumentInfobarProps {
|
||||
changedAuthor: string
|
||||
changedTime: number
|
||||
changedTime: DateTime
|
||||
createdAuthor: string
|
||||
createdTime: number
|
||||
createdTime: DateTime
|
||||
editable: boolean
|
||||
noteId: string
|
||||
viewCount: number
|
||||
|
@ -43,12 +43,12 @@ export const DocumentInfobar: React.FC<DocumentInfobarProps> = ({
|
|||
<div className={'d-flex flex-column'}>
|
||||
<DocumentInfoTimeLine
|
||||
mode={DocumentInfoLineWithTimeMode.CREATED}
|
||||
time={ DateTime.fromSeconds(createdTime) }
|
||||
time={createdTime}
|
||||
userName={createdAuthor}
|
||||
profileImageSrc={'/img/avatar.png'}/>
|
||||
<DocumentInfoTimeLine
|
||||
mode={DocumentInfoLineWithTimeMode.EDITED}
|
||||
time={ DateTime.fromSeconds(changedTime) }
|
||||
time={changedTime}
|
||||
userName={changedAuthor}
|
||||
profileImageSrc={'/img/avatar.png'}/>
|
||||
<hr/>
|
||||
|
|
|
@ -4,99 +4,61 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useParams } from 'react-router'
|
||||
import { getNote, Note } from '../../api/notes'
|
||||
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 { setDocumentContent, setDocumentMetadata } from '../../redux/document-content/methods'
|
||||
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
|
||||
import { setNoteFrontmatter, updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
||||
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
||||
import { ShowIf } from '../common/show-if/show-if'
|
||||
import { AppBar, AppBarMode } from '../editor/app-bar/app-bar'
|
||||
import { DocumentIframe } from '../editor/document-renderer-pane/document-iframe'
|
||||
import { EditorPathParams } from '../editor/editor'
|
||||
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
|
||||
import { useLoadNoteFromServer } from '../editor/useLoadNoteFromServer'
|
||||
import { DocumentInfobar } from './document-infobar'
|
||||
import { ErrorWhileLoadingNoteAlert } from './ErrorWhileLoadingNoteAlert'
|
||||
import { LoadingNoteAlert } from './LoadingNoteAlert'
|
||||
|
||||
export const PadViewOnly: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
useTranslation()
|
||||
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()
|
||||
useDocumentTitle(documentTitle)
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
useDocumentTitleWithNoteTitle()
|
||||
|
||||
const onFirstHeadingChange = useCallback(updateNoteTitleByFirstHeading, [])
|
||||
const onFrontmatterChange = useCallback(setNoteFrontmatter, [])
|
||||
const [error, loading] = useLoadNoteFromServer()
|
||||
const markdownContent = useNoteMarkdownContent()
|
||||
const noteDetails = useSelector((state: ApplicationState) => state.noteDetails)
|
||||
|
||||
return (
|
||||
<div className={'d-flex flex-column mvh-100 bg-light'}>
|
||||
<MotdBanner/>
|
||||
<AppBar mode={AppBarMode.BASIC}/>
|
||||
<div className={'container'}>
|
||||
<ShowIf condition={error}>
|
||||
<Alert variant={'danger'} className={'my-2'}>
|
||||
<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>
|
||||
<ErrorWhileLoadingNoteAlert show={error}/>
|
||||
<LoadingNoteAlert show={loading}/>
|
||||
</div>
|
||||
<ShowIf condition={!error && !loading}>
|
||||
{ /* TODO set editable and created author properly */}
|
||||
<DocumentInfobar
|
||||
changedAuthor={noteData?.lastChange.userId ?? ''}
|
||||
changedTime={noteData?.lastChange.timestamp ?? 0}
|
||||
changedAuthor={noteDetails.lastChange.userId ?? ''}
|
||||
changedTime={noteDetails.lastChange.timestamp}
|
||||
createdAuthor={'Test'}
|
||||
createdTime={noteData?.createtime ?? 0}
|
||||
createdTime={noteDetails.createTime}
|
||||
editable={true}
|
||||
noteId={id}
|
||||
viewCount={noteData?.viewcount ?? 0}
|
||||
viewCount={noteDetails.viewCount}
|
||||
/>
|
||||
<DocumentIframe extraClasses={"flex-fill"}
|
||||
markdownContent={markdownContent}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onMetadataChange={onMetadataChange}/>
|
||||
onFrontmatterChange={onFrontmatterChange}/>
|
||||
</ShowIf>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { NoteFrontmatter } from "../editor/note-frontmatter/note-frontmatter"
|
||||
import { ScrollState } from "../editor/scroll/scroll-props"
|
||||
import { YAMLMetaData } from "../editor/yaml-metadata/yaml-metadata"
|
||||
import { IframeCommunicator } from "./iframe-communicator"
|
||||
import {
|
||||
EditorToRendererIframeMessage,
|
||||
|
@ -18,36 +18,13 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<Edito
|
|||
private onSetScrollSourceToRendererHandler?: () => void
|
||||
private onTaskCheckboxChangeHandler?: (lineInMarkdown: number, checked: boolean) => void
|
||||
private onFirstHeadingChangeHandler?: (heading?: string) => void
|
||||
private onMetaDataChangeHandler?: (metaData?: YAMLMetaData) => void
|
||||
private onFrontmatterChangeHandler?: (metaData?: NoteFrontmatter) => void
|
||||
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
|
||||
private onRendererReadyHandler?: () => void
|
||||
private onImageClickedHandler?: (details: ImageDetails) => void
|
||||
|
||||
protected handleEvent (event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined {
|
||||
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_META_DATA:
|
||||
this.onMetaDataChangeHandler?.(renderMessage.metaData)
|
||||
return false
|
||||
case RenderIframeMessageType.IMAGE_CLICKED:
|
||||
this.onImageClickedHandler?.(renderMessage.details)
|
||||
return false
|
||||
}
|
||||
public onFrontmatterChange (handler?: (frontmatter?: NoteFrontmatter) => void): void {
|
||||
this.onFrontmatterChangeHandler = handler
|
||||
}
|
||||
|
||||
public onImageClicked (handler?: (details: ImageDetails) => void): void {
|
||||
|
@ -70,8 +47,31 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<Edito
|
|||
this.onFirstHeadingChangeHandler = handler
|
||||
}
|
||||
|
||||
public onMetaDataChange (handler?: (metaData?: YAMLMetaData) => void): void {
|
||||
this.onMetaDataChangeHandler = handler
|
||||
protected handleEvent (event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined {
|
||||
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 {
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { NoteFrontmatter } from "../editor/note-frontmatter/note-frontmatter"
|
||||
import { ScrollState } from "../editor/scroll/scroll-props"
|
||||
import { YAMLMetaData } from "../editor/yaml-metadata/yaml-metadata"
|
||||
import { IframeCommunicator } from "./iframe-communicator"
|
||||
import {
|
||||
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({
|
||||
type: RenderIframeMessageType.ON_SET_META_DATA,
|
||||
metaData
|
||||
type: RenderIframeMessageType.ON_SET_FRONTMATTER,
|
||||
frontmatter: frontmatter
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -3,16 +3,15 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import equal from "fast-deep-equal"
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
||||
import { ApplicationState } from '../../redux'
|
||||
import { setDarkMode } from '../../redux/dark-mode/methods'
|
||||
import { setDocumentMetadata } from '../../redux/document-content/methods'
|
||||
import { ScrollingDocumentRenderPane } from '../editor/document-renderer-pane/scrolling-document-render-pane'
|
||||
import { setNoteFrontmatter } from '../../redux/note-details/methods'
|
||||
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 { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
|
||||
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
||||
import { IframeRendererToEditorCommunicator } from './iframe-renderer-to-editor-communicator'
|
||||
|
||||
|
@ -41,11 +40,7 @@ export const RenderPage: React.FC = () => {
|
|||
useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator.onSetWide(setWide), [iframeCommunicator])
|
||||
useEffect(() => iframeCommunicator.onSetScrollState((newScrollState) => {
|
||||
if (!equal(scrollState, newScrollState)) {
|
||||
setScrollState(newScrollState)
|
||||
}
|
||||
}), [iframeCommunicator, scrollState])
|
||||
useEffect(() => iframeCommunicator.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
|
||||
|
||||
const onTaskCheckedChange = useCallback((lineInMarkdown: number, checked: boolean) => {
|
||||
iframeCommunicator.sendTaskCheckBoxChange(lineInMarkdown, checked)
|
||||
|
@ -59,9 +54,9 @@ export const RenderPage: React.FC = () => {
|
|||
iframeCommunicator.sendSetScrollSourceToRenderer()
|
||||
}, [iframeCommunicator])
|
||||
|
||||
const onMetaDataChange = useCallback((metaData?: YAMLMetaData) => {
|
||||
setDocumentMetadata(metaData)
|
||||
iframeCommunicator.sendSetMetaData(metaData)
|
||||
const onFrontmatterChange = useCallback((frontmatter?: NoteFrontmatter) => {
|
||||
setNoteFrontmatter(frontmatter)
|
||||
iframeCommunicator.sendSetFrontmatter(frontmatter)
|
||||
}, [iframeCommunicator])
|
||||
|
||||
const onScroll = useCallback((scrollState: ScrollState) => {
|
||||
|
@ -86,14 +81,14 @@ export const RenderPage: React.FC = () => {
|
|||
|
||||
return (
|
||||
<div className={"vh-100 w-100"}>
|
||||
<ScrollingDocumentRenderPane
|
||||
<DocumentRenderPane
|
||||
extraClasses={'w-100'}
|
||||
markdownContent={markdownContent}
|
||||
wide={isWide}
|
||||
onTaskCheckedChange={onTaskCheckedChange}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onMakeScrollSource={onMakeScrollSource}
|
||||
onMetadataChange={onMetaDataChange}
|
||||
onFrontmatterChange={onFrontmatterChange}
|
||||
scrollState={scrollState}
|
||||
onScroll={onScroll}
|
||||
baseUrl={baseUrl}
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { NoteFrontmatter } from '../editor/note-frontmatter/note-frontmatter'
|
||||
import { ScrollState } from '../editor/scroll/scroll-props'
|
||||
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
|
||||
|
||||
export enum RenderIframeMessageType {
|
||||
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
||||
|
@ -15,7 +15,7 @@ export enum RenderIframeMessageType {
|
|||
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
|
||||
SET_SCROLL_SOURCE_TO_RENDERER = 'SET_SCROLL_SOURCE_TO_RENDERER',
|
||||
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
||||
ON_SET_META_DATA = 'ON_SET_META_DATA',
|
||||
ON_SET_FRONTMATTER = 'ON_SET_FRONTMATTER',
|
||||
IMAGE_CLICKED = 'IMAGE_CLICKED',
|
||||
SET_BASE_URL = 'SET_BASE_URL'
|
||||
}
|
||||
|
@ -71,9 +71,9 @@ export interface OnFirstHeadingChangeMessage {
|
|||
firstHeading: string | undefined
|
||||
}
|
||||
|
||||
export interface OnMetadataChangeMessage {
|
||||
type: RenderIframeMessageType.ON_SET_META_DATA,
|
||||
metaData: YAMLMetaData | undefined
|
||||
export interface OnFrontmatterChangeMessage {
|
||||
type: RenderIframeMessageType.ON_SET_FRONTMATTER,
|
||||
frontmatter: NoteFrontmatter | undefined
|
||||
}
|
||||
|
||||
export type EditorToRendererIframeMessage =
|
||||
|
@ -87,6 +87,6 @@ export type RendererToEditorIframeMessage =
|
|||
RendererToEditorSimpleMessage |
|
||||
OnFirstHeadingChangeMessage |
|
||||
OnTaskCheckboxChangeMessage |
|
||||
OnMetadataChangeMessage |
|
||||
OnFrontmatterChangeMessage |
|
||||
SetScrollStateMessage |
|
||||
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-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
@ -86,7 +86,7 @@ ReactDOM.render(
|
|||
)
|
||||
|
||||
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
|
||||
|
|
|
@ -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 { DarkModeConfigReducer } from './dark-mode/reducers'
|
||||
import { DarkModeConfig } from './dark-mode/types'
|
||||
import { DocumentContentReducer } from './document-content/reducers'
|
||||
import { DocumentContent } from './document-content/types'
|
||||
import { EditorConfigReducer } from './editor/reducers'
|
||||
import { EditorConfig } from './editor/types'
|
||||
import { NoteDetailsReducer } from './note-details/reducers'
|
||||
import { NoteDetails } from './note-details/types'
|
||||
import { UserReducer } from './user/reducers'
|
||||
import { MaybeUserState } from './user/types'
|
||||
|
||||
|
@ -27,7 +27,7 @@ export interface ApplicationState {
|
|||
apiUrl: ApiUrlObject;
|
||||
editorConfig: EditorConfig;
|
||||
darkMode: DarkModeConfig;
|
||||
documentContent: DocumentContent;
|
||||
noteDetails: NoteDetails;
|
||||
}
|
||||
|
||||
export const allReducers: Reducer<ApplicationState> = combineReducers<ApplicationState>({
|
||||
|
@ -37,7 +37,7 @@ export const allReducers: Reducer<ApplicationState> = combineReducers<Applicatio
|
|||
apiUrl: ApiUrlReducer,
|
||||
editorConfig: EditorConfigReducer,
|
||||
darkMode: DarkModeConfigReducer,
|
||||
documentContent: DocumentContentReducer
|
||||
noteDetails: NoteDetailsReducer
|
||||
})
|
||||
|
||||
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