mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-06-02 07:59:56 -04:00
Added synced scrolling (#386)
Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
164b5436ae
commit
73007ef597
10 changed files with 413 additions and 38 deletions
|
@ -1,12 +1,14 @@
|
|||
import React, { useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
import { TocAst } from '../../../external-types/markdown-it-toc-done-right/interface'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { MarkdownRenderer } from '../../markdown-renderer/markdown-renderer'
|
||||
import { YAMLMetaData } from '../yaml-metadata/yaml-metadata'
|
||||
import { LineMarkerPosition, MarkdownRenderer } from '../../markdown-renderer/markdown-renderer'
|
||||
import { ScrollProps, ScrollState } from '../scroll/scroll-props'
|
||||
import { findLineMarks } from '../scroll/utils'
|
||||
import { TableOfContents } from '../table-of-contents/table-of-contents'
|
||||
import { YAMLMetaData } from '../yaml-metadata/yaml-metadata'
|
||||
|
||||
interface DocumentRenderPaneProps {
|
||||
content: string
|
||||
|
@ -15,15 +17,69 @@ interface DocumentRenderPaneProps {
|
|||
wide?: boolean
|
||||
}
|
||||
|
||||
export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = ({ content, onMetadataChange, onFirstHeadingChange, wide }) => {
|
||||
export const DocumentRenderPane: React.FC<DocumentRenderPaneProps & ScrollProps> = ({ content, onMetadataChange, onFirstHeadingChange, wide, scrollState, onScroll, onMakeScrollSource }) => {
|
||||
const [tocAst, setTocAst] = useState<TocAst>()
|
||||
const renderer = useRef<HTMLDivElement>(null)
|
||||
const { width } = useResizeObserver({ ref: renderer })
|
||||
const lastScrollPosition = useRef<number>()
|
||||
const [lineMarks, setLineMarks] = useState<LineMarkerPosition[]>()
|
||||
|
||||
const realWidth = width || 0
|
||||
|
||||
useEffect(() => {
|
||||
if (!renderer.current || !lineMarks || !scrollState) {
|
||||
return
|
||||
}
|
||||
const { lastMarkBefore, firstMarkAfter } = findLineMarks(lineMarks, scrollState.firstLineInView)
|
||||
const positionBefore = lastMarkBefore ? lastMarkBefore.position : 0
|
||||
const positionAfter = firstMarkAfter ? firstMarkAfter.position : renderer.current.offsetHeight
|
||||
const lastMarkBeforeLine = lastMarkBefore ? lastMarkBefore.line : 1
|
||||
const firstMarkAfterLine = firstMarkAfter ? firstMarkAfter.line : content.split('\n').length
|
||||
const lineCount = firstMarkAfterLine - lastMarkBeforeLine
|
||||
const blockHeight = positionAfter - positionBefore
|
||||
const lineHeight = blockHeight / lineCount
|
||||
const position = positionBefore + (scrollState.firstLineInView - lastMarkBeforeLine) * lineHeight + scrollState.scrolledPercentage / 100 * lineHeight
|
||||
const correctedPosition = Math.floor(position)
|
||||
if (correctedPosition !== lastScrollPosition.current) {
|
||||
lastScrollPosition.current = correctedPosition
|
||||
renderer.current.scrollTo({
|
||||
top: correctedPosition
|
||||
})
|
||||
}
|
||||
}, [content, lineMarks, scrollState])
|
||||
|
||||
const userScroll = useCallback(() => {
|
||||
if (!renderer.current || !lineMarks || !onScroll) {
|
||||
return
|
||||
}
|
||||
const resyncedScroll = Math.ceil(renderer.current.scrollTop) === lastScrollPosition.current
|
||||
if (resyncedScroll) {
|
||||
return
|
||||
}
|
||||
|
||||
const scrollTop = renderer.current.scrollTop
|
||||
|
||||
const beforeLineMark = lineMarks
|
||||
.filter(lineMark => lineMark.position <= scrollTop)
|
||||
.reduce((prevLineMark, currentLineMark) =>
|
||||
prevLineMark.line >= currentLineMark.line ? prevLineMark : currentLineMark)
|
||||
|
||||
const afterLineMark = lineMarks
|
||||
.filter(lineMark => lineMark.position > scrollTop)
|
||||
.reduce((prevLineMark, currentLineMark) =>
|
||||
prevLineMark.line < currentLineMark.line ? prevLineMark : currentLineMark)
|
||||
|
||||
const blockHeight = afterLineMark.position - beforeLineMark.position
|
||||
const distanceToBefore = scrollTop - beforeLineMark.position
|
||||
const percentageRaw = (distanceToBefore / blockHeight)
|
||||
const percentage = Math.floor(percentageRaw * 100)
|
||||
const newScrollState: ScrollState = { firstLineInView: beforeLineMark.line, scrolledPercentage: percentage }
|
||||
onScroll(newScrollState)
|
||||
}, [lineMarks, onScroll])
|
||||
|
||||
return (
|
||||
<div className={'bg-light flex-fill pb-5 flex-row d-flex w-100 h-100 overflow-y-scroll'} ref={renderer}>
|
||||
<div className={'bg-light flex-fill pb-5 flex-row d-flex w-100 h-100 overflow-y-scroll'}
|
||||
ref={renderer} onScroll={userScroll} onMouseEnter={onMakeScrollSource} >
|
||||
<div className={'col-md'}/>
|
||||
<MarkdownRenderer
|
||||
className={'flex-fill'}
|
||||
|
@ -32,11 +88,12 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = ({ content,
|
|||
onTocChange={(tocAst) => setTocAst(tocAst)}
|
||||
onMetaDataChange={onMetadataChange}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onLineMarkerPositionChanged={setLineMarks}
|
||||
/>
|
||||
|
||||
<div className={'col-md'}>
|
||||
<ShowIf condition={realWidth >= 1280 && !!tocAst}>
|
||||
<TableOfContents ast={tocAst as TocAst} sticky={true}/>
|
||||
<TableOfContents ast={tocAst as TocAst} className={'position-fixed'}/>
|
||||
</ShowIf>
|
||||
<ShowIf condition={realWidth < 1280 && !!tocAst}>
|
||||
<div className={'markdown-toc-sidebar-button'}>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Editor, EditorChange, EditorConfiguration } from 'codemirror'
|
||||
import { Editor, EditorChange, EditorConfiguration, ScrollInfo } from 'codemirror'
|
||||
import 'codemirror/addon/comment/comment'
|
||||
import 'codemirror/addon/dialog/dialog'
|
||||
import 'codemirror/addon/display/autorefresh'
|
||||
|
@ -20,10 +20,11 @@ import 'codemirror/keymap/sublime'
|
|||
import 'codemirror/keymap/emacs'
|
||||
import 'codemirror/keymap/vim'
|
||||
import 'codemirror/mode/gfm/gfm'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import './editor-pane.scss'
|
||||
import { ScrollProps, ScrollState } from '../scroll/scroll-props'
|
||||
import { generateEmojiHints, emojiWordRegex, findWordAtCursor } from './hints/emoji'
|
||||
import { defaultKeyMap } from './key-map'
|
||||
import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar'
|
||||
|
@ -48,7 +49,7 @@ const onChange = (editor: Editor) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const EditorPane: React.FC<EditorPaneProps> = ({ onContentChange, content }) => {
|
||||
export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentChange, content, scrollState, onScroll, onMakeScrollSource }) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor, setEditor] = useState<Editor>()
|
||||
const [statusBarInfo, setStatusBarInfo] = useState<StatusBarInfo>(defaultState)
|
||||
|
@ -59,6 +60,41 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ onContentChange, content
|
|||
indentWithTabs: false
|
||||
})
|
||||
|
||||
const lastScrollPosition = useRef<number>()
|
||||
|
||||
const onEditorScroll = useCallback((editor: Editor, data: ScrollInfo) => {
|
||||
if (!editor || !onScroll) {
|
||||
return
|
||||
}
|
||||
const scrollEventValue = data.top as number
|
||||
const line = editor.lineAtHeight(scrollEventValue, 'local')
|
||||
const startYOfLine = editor.heightAtLine(line, 'local')
|
||||
const lineInfo = editor.lineInfo(line)
|
||||
if (lineInfo === null) {
|
||||
return
|
||||
}
|
||||
const heightOfLine = (lineInfo.handle as { height: number }).height
|
||||
const percentageRaw = (Math.max(scrollEventValue - startYOfLine, 0)) / heightOfLine
|
||||
const percentage = Math.floor(percentageRaw * 100)
|
||||
|
||||
const newScrollState: ScrollState = { firstLineInView: line + 1, scrolledPercentage: percentage }
|
||||
onScroll(newScrollState)
|
||||
}, [onScroll])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor || !scrollState) {
|
||||
return
|
||||
}
|
||||
const startYOfLine = editor.heightAtLine(scrollState.firstLineInView - 1, 'div')
|
||||
const heightOfLine = (editor.lineInfo(scrollState.firstLineInView - 1).handle as { height: number }).height
|
||||
const newPositionRaw = startYOfLine + (heightOfLine * scrollState.scrolledPercentage / 100)
|
||||
const newPosition = Math.floor(newPositionRaw)
|
||||
if (newPosition !== lastScrollPosition.current) {
|
||||
lastScrollPosition.current = newPosition
|
||||
editor.scrollTo(0, newPosition)
|
||||
}
|
||||
}, [editor, scrollState])
|
||||
|
||||
const onBeforeChange = useCallback((editor: Editor, data: EditorChange, value: string) => {
|
||||
onContentChange(value)
|
||||
}, [onContentChange])
|
||||
|
@ -100,7 +136,7 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ onContentChange, content
|
|||
}), [t, editorPreferences])
|
||||
|
||||
return (
|
||||
<div className={'d-flex flex-column h-100'}>
|
||||
<div className={'d-flex flex-column h-100'} onMouseEnter={onMakeScrollSource}>
|
||||
<ToolBar
|
||||
editor={editor}
|
||||
onPreferencesChange={config => setEditorPreferences(config)}
|
||||
|
@ -114,6 +150,7 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ onContentChange, content
|
|||
onCursorActivity={onCursorActivity}
|
||||
editorDidMount={onEditorDidMount}
|
||||
onBeforeChange={onBeforeChange}
|
||||
onScroll={onEditorScroll}
|
||||
/>
|
||||
<StatusBar {...statusBarInfo} />
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Splitter } from './splitter/splitter'
|
|||
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
||||
import { DocumentBar } from './document-bar/document-bar'
|
||||
import { editorTestContent } from './editorTestContent'
|
||||
import { DualScrollState, ScrollState } from './scroll/scroll-props'
|
||||
import { YAMLMetaData } from './yaml-metadata/yaml-metadata'
|
||||
import { AppBar } from './app-bar/app-bar'
|
||||
import { EditorMode } from './app-bar/editor-view-mode'
|
||||
|
@ -19,6 +20,11 @@ export interface EditorPathParams {
|
|||
id: string
|
||||
}
|
||||
|
||||
export enum ScrollSource {
|
||||
EDITOR,
|
||||
RENDERER
|
||||
}
|
||||
|
||||
export const Editor: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const untitledNote = t('editor.untitledNote')
|
||||
|
@ -29,6 +35,12 @@ export const Editor: React.FC = () => {
|
|||
const [documentTitle, setDocumentTitle] = useState(untitledNote)
|
||||
const noteMetadata = useRef<YAMLMetaData>()
|
||||
const firstHeading = useRef<string>()
|
||||
const scrollSource = useRef<ScrollSource>(ScrollSource.EDITOR)
|
||||
|
||||
const [scrollState, setScrollState] = useState<DualScrollState>(() => ({
|
||||
editorScrollState: { firstLineInView: 1, scrolledPercentage: 0 },
|
||||
rendererScrollState: { firstLineInView: 1, scrolledPercentage: 0 }
|
||||
}))
|
||||
|
||||
const updateDocumentTitle = useCallback(() => {
|
||||
if (noteMetadata.current?.title && noteMetadata.current?.title !== '') {
|
||||
|
@ -60,6 +72,18 @@ export const Editor: React.FC = () => {
|
|||
}
|
||||
}, [editorMode, firstDraw, isWide])
|
||||
|
||||
const onEditorScroll = useCallback((newScrollState: ScrollState) => {
|
||||
if (scrollSource.current === ScrollSource.EDITOR) {
|
||||
setScrollState((old) => ({ rendererScrollState: newScrollState, editorScrollState: old.editorScrollState }))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onMarkdownRendererScroll = useCallback((newScrollState: ScrollState) => {
|
||||
if (scrollSource.current === ScrollSource.RENDERER) {
|
||||
setScrollState((old) => ({ editorScrollState: newScrollState, rendererScrollState: old.rendererScrollState }))
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<MotdBanner/>
|
||||
|
@ -72,16 +96,22 @@ export const Editor: React.FC = () => {
|
|||
left={
|
||||
<EditorPane
|
||||
onContentChange={content => setMarkdownContent(content)}
|
||||
content={markdownContent}/>
|
||||
content={markdownContent}
|
||||
scrollState={scrollState.editorScrollState}
|
||||
onScroll={onEditorScroll}
|
||||
onMakeScrollSource={() => { scrollSource.current = ScrollSource.EDITOR }}
|
||||
/>
|
||||
}
|
||||
showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)}
|
||||
right={
|
||||
<DocumentRenderPane
|
||||
content={markdownContent}
|
||||
wide={editorMode === EditorMode.PREVIEW}
|
||||
scrollState={scrollState.rendererScrollState}
|
||||
onScroll={onMarkdownRendererScroll}
|
||||
onMetadataChange={onMetadataChange}
|
||||
onFirstHeadingChange={onFirstHeadingChange}/>
|
||||
}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onMakeScrollSource={() => { scrollSource.current = ScrollSource.RENDERER }}/>}
|
||||
containerClassName={'overflow-hidden'}/>
|
||||
</div>
|
||||
</Fragment>
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import MarkdownIt from 'markdown-it/lib'
|
||||
import Token from 'markdown-it/lib/token'
|
||||
|
||||
export const lineNumberMarker: () => MarkdownIt.PluginSimple = () => {
|
||||
const endMarkers: number[] = []
|
||||
|
||||
return (md: MarkdownIt) => {
|
||||
// add codimd_linemarker token before each opening or self-closing level-0 tag
|
||||
md.core.ruler.push('line_number_marker', (state) => {
|
||||
for (let i = 0; i < state.tokens.length; i++) {
|
||||
const token = state.tokens[i]
|
||||
if (token.level !== 0) {
|
||||
continue
|
||||
}
|
||||
if (token.nesting !== -1) {
|
||||
if (!token.map) {
|
||||
continue
|
||||
}
|
||||
const lineNumber = token.map[0] + 1
|
||||
if (token.nesting === 1) {
|
||||
endMarkers.push(token.map[1] + 1)
|
||||
}
|
||||
const tokenBefore = state.tokens[i - 1]
|
||||
if (tokenBefore === undefined || tokenBefore.type !== 'codimd_linemarker' || tokenBefore.tag !== 'codimd-linemarker' ||
|
||||
tokenBefore.attrGet('data-linenumber') !== `${lineNumber}`) {
|
||||
const startToken = new Token('codimd_linemarker', 'codimd-linemarker', 0)
|
||||
startToken.attrPush(['data-linenumber', `${lineNumber}`])
|
||||
state.tokens.splice(i, 0, startToken)
|
||||
i += 1
|
||||
}
|
||||
} else {
|
||||
const lineNumber = endMarkers.pop()
|
||||
if (lineNumber) {
|
||||
const startToken = new Token('codimd_linemarker', 'codimd-linemarker', 0)
|
||||
startToken.attrPush(['data-linenumber', `${lineNumber}`])
|
||||
state.tokens.splice(i + 1, 0, startToken)
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// render codimd_linemarker token to <codimd-linemarker></codimd-linemarker>
|
||||
md.renderer.rules.codimd_linemarker = (tokens: Token[], index: number): string => {
|
||||
const lineNumber = tokens[index].attrGet('data-linenumber')
|
||||
if (!lineNumber) {
|
||||
// don't render broken linemarkers without a linenumber
|
||||
return ''
|
||||
}
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<codimd-linemarker data-linenumber='${lineNumber}'></codimd-linemarker>`
|
||||
}
|
||||
}
|
||||
}
|
15
src/components/editor/scroll/scroll-props.ts
Normal file
15
src/components/editor/scroll/scroll-props.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export interface ScrollProps {
|
||||
scrollState?: ScrollState
|
||||
onScroll?: (scrollState: ScrollState) => void
|
||||
onMakeScrollSource?: () => void
|
||||
}
|
||||
|
||||
export interface ScrollState {
|
||||
firstLineInView: number
|
||||
scrolledPercentage: number
|
||||
}
|
||||
|
||||
export interface DualScrollState {
|
||||
editorScrollState: ScrollState
|
||||
rendererScrollState: ScrollState
|
||||
}
|
25
src/components/editor/scroll/utils.ts
Normal file
25
src/components/editor/scroll/utils.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { LineMarkerPosition } from '../../markdown-renderer/markdown-renderer'
|
||||
export const findLineMarks = (lineMarks: LineMarkerPosition[], lineNumber: number): { lastMarkBefore: LineMarkerPosition | undefined, firstMarkAfter: LineMarkerPosition | undefined } => {
|
||||
let lastMarkBefore
|
||||
let firstMarkAfter
|
||||
for (let i = 0; i < lineMarks.length; i++) {
|
||||
const currentMark = lineMarks[i]
|
||||
if (!currentMark) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (currentMark.line <= lineNumber) {
|
||||
lastMarkBefore = currentMark
|
||||
}
|
||||
if (currentMark.line > lineNumber) {
|
||||
firstMarkAfter = currentMark
|
||||
}
|
||||
if (!!firstMarkAfter && !!lastMarkBefore) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return {
|
||||
lastMarkBefore,
|
||||
firstMarkAfter
|
||||
}
|
||||
}
|
|
@ -54,7 +54,7 @@ export const Splitter: React.FC<SplitterProps> = ({ containerClassName, left, ri
|
|||
</div>
|
||||
</ShowIf>
|
||||
<ShowIf condition={showRight}>
|
||||
<div className='splitter right'>
|
||||
<div className={'splitter right'}>
|
||||
{right}
|
||||
</div>
|
||||
</ShowIf>
|
||||
|
|
|
@ -6,7 +6,7 @@ import './table-of-contents.scss'
|
|||
export interface TableOfContentsProps {
|
||||
ast: TocAst
|
||||
maxDepth?: number
|
||||
sticky?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const slugify = (content:string) => {
|
||||
|
@ -51,11 +51,11 @@ const convertLevel = (toc: TocAst, levelsToShowUnderThis: number, headerCounts:
|
|||
}
|
||||
}
|
||||
|
||||
export const TableOfContents: React.FC<TableOfContentsProps> = ({ ast, maxDepth = 3, sticky }) => {
|
||||
export const TableOfContents: React.FC<TableOfContentsProps> = ({ ast, maxDepth = 3, className }) => {
|
||||
const tocTree = useMemo(() => convertLevel(ast, maxDepth, new Map<string, number>(), false), [ast, maxDepth])
|
||||
|
||||
return (
|
||||
<div className={`markdown-toc ${sticky ? 'sticky' : ''}`}>
|
||||
<div className={`markdown-toc ${className ?? ''}`}>
|
||||
{tocTree}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -21,11 +21,12 @@ import subscript from 'markdown-it-sub'
|
|||
import superscript from 'markdown-it-sup'
|
||||
import taskList from 'markdown-it-task-lists'
|
||||
import toc from 'markdown-it-toc-done-right'
|
||||
import React, { ReactElement, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import ReactHtmlParser, { convertNodeToElement, Transform } from 'react-html-parser'
|
||||
import { Trans } from 'react-i18next'
|
||||
import MathJaxReact from 'react-mathjax'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { TocAst } from '../../external-types/markdown-it-toc-done-right/interface'
|
||||
import { ApplicationState } from '../../redux'
|
||||
|
@ -65,6 +66,12 @@ import { QuoteOptionsReplacer } from './replace-components/quote-options/quote-o
|
|||
import { TocReplacer } from './replace-components/toc/toc-replacer'
|
||||
import { VimeoReplacer } from './replace-components/vimeo/vimeo-replacer'
|
||||
import { YoutubeReplacer } from './replace-components/youtube/youtube-replacer'
|
||||
import { lineNumberMarker } from '../editor/markdown-renderer/markdown-it-plugins/line-number-marker'
|
||||
|
||||
export interface LineMarkerPosition {
|
||||
line: number
|
||||
position: number
|
||||
}
|
||||
|
||||
export interface MarkdownRendererProps {
|
||||
content: string
|
||||
|
@ -73,6 +80,7 @@ export interface MarkdownRendererProps {
|
|||
onTocChange?: (ast: TocAst) => void
|
||||
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||
}
|
||||
|
||||
const markdownItTwitterEmojis = Object.keys((emojiData as unknown as Data).emojis)
|
||||
|
@ -102,14 +110,41 @@ const forkAwesomeIconMap = Object.keys(ForkAwesomeIcons)
|
|||
return reduceObject
|
||||
}, {} as { [key: string]: string })
|
||||
|
||||
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide }) => {
|
||||
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide, onLineMarkerPositionChanged }) => {
|
||||
const [tocAst, setTocAst] = useState<TocAst>()
|
||||
const [lastTocAst, setLastTocAst] = useState<TocAst>()
|
||||
const lastTocAst = useRef<TocAst>()
|
||||
const [yamlError, setYamlError] = useState(false)
|
||||
const rawMetaRef = useRef<RawYAMLMetadata>()
|
||||
const oldMetaRef = useRef<RawYAMLMetadata>()
|
||||
const firstHeadingRef = useRef<string>()
|
||||
const oldFirstHeadingRef = useRef<string>()
|
||||
const documentElement = useRef<HTMLDivElement>(null)
|
||||
const lastLineMarkerPositions = useRef<LineMarkerPosition[]>()
|
||||
|
||||
const calculateLineMarkerPositions = useCallback(() => {
|
||||
if (documentElement.current && onLineMarkerPositionChanged) {
|
||||
const lineMarkers: NodeListOf<HTMLDivElement> = documentElement.current.querySelectorAll('codimd-linemarker')
|
||||
const lineMarkerPositions: LineMarkerPosition[] = Array.from(lineMarkers).map((marker) => {
|
||||
return {
|
||||
line: Number(marker.getAttribute('data-linenumber')),
|
||||
position: marker.offsetTop
|
||||
} as LineMarkerPosition
|
||||
})
|
||||
if (!equal(lineMarkerPositions, lastLineMarkerPositions.current)) {
|
||||
lastLineMarkerPositions.current = lineMarkerPositions
|
||||
onLineMarkerPositionChanged(lineMarkerPositions)
|
||||
}
|
||||
}
|
||||
}, [onLineMarkerPositionChanged])
|
||||
|
||||
useEffect(() => {
|
||||
calculateLineMarkerPositions()
|
||||
}, [calculateLineMarkerPositions])
|
||||
|
||||
useResizeObserver({
|
||||
ref: documentElement,
|
||||
onResize: () => calculateLineMarkerPositions()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (onMetaDataChange && !equal(oldMetaRef.current, rawMetaRef.current)) {
|
||||
|
@ -239,6 +274,7 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onM
|
|||
slugify: slugify
|
||||
})
|
||||
md.use(linkifyExtra)
|
||||
md.use(lineNumberMarker())
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
md.use(MarkdownItParserDebugger)
|
||||
}
|
||||
|
@ -251,11 +287,11 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onM
|
|||
}, [onMetaDataChange, onFirstHeadingChange, plantumlServer])
|
||||
|
||||
useEffect(() => {
|
||||
if (onTocChange && tocAst && !equal(tocAst, lastTocAst)) {
|
||||
if (onTocChange && tocAst && !equal(tocAst, lastTocAst.current)) {
|
||||
lastTocAst.current = tocAst
|
||||
onTocChange(tocAst)
|
||||
setLastTocAst(tocAst)
|
||||
}
|
||||
}, [tocAst, onTocChange, lastTocAst])
|
||||
}, [tocAst, onTocChange])
|
||||
|
||||
const tryToReplaceNode = (node: DomElement, index: number, allReplacers: ComponentReplacer[], nodeConverter: SubNodeConverter) => {
|
||||
return allReplacers
|
||||
|
@ -291,17 +327,19 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onM
|
|||
}, [content, markdownIt, onMetaDataChange])
|
||||
|
||||
return (
|
||||
<div className={`markdown-body ${className || ''} d-flex flex-column align-items-center ${wide ? 'wider' : ''}`}>
|
||||
<ShowIf condition={yamlError}>
|
||||
<Alert variant='warning' dir='auto'>
|
||||
<Trans i18nKey='editor.invalidYaml'>
|
||||
<InternalLink text='yaml-metadata' href='/n/yaml-metadata' className='text-dark'/>
|
||||
</Trans>
|
||||
</Alert>
|
||||
</ShowIf>
|
||||
<MathJaxReact.Provider>
|
||||
{result}
|
||||
</MathJaxReact.Provider>
|
||||
<div className={'bg-light flex-fill pb-5'}>
|
||||
<div className={`markdown-body ${className || ''} d-flex flex-column align-items-center ${wide ? 'wider' : ''}`} ref={documentElement}>
|
||||
<ShowIf condition={yamlError}>
|
||||
<Alert variant='warning' dir='auto'>
|
||||
<Trans i18nKey='editor.invalidYaml'>
|
||||
<InternalLink text='yaml-metadata' href='/n/yaml-metadata' className='text-dark'/>
|
||||
</Trans>
|
||||
</Alert>
|
||||
</ShowIf>
|
||||
<MathJaxReact.Provider>
|
||||
{result}
|
||||
</MathJaxReact.Provider>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue