mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-15 07:34:42 -04:00
fix: Move content into to frontend directory
Doing this BEFORE the merge prevents a lot of merge conflicts. Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
4e18ce38f3
commit
762a0a850e
1051 changed files with 0 additions and 35 deletions
31
frontend/src/components/render-page/document-toc-sidebar.tsx
Normal file
31
frontend/src/components/render-page/document-toc-sidebar.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import styles from './markdown-document.module.scss'
|
||||
import { ShowIf } from '../common/show-if/show-if'
|
||||
import { WidthBasedTableOfContents } from './width-based-table-of-contents'
|
||||
import type { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { useExtensionEventEmitterHandler } from '../markdown-renderer/hooks/use-extension-event-emitter'
|
||||
import { TableOfContentsMarkdownExtension } from '../markdown-renderer/extensions/table-of-contents-markdown-extension'
|
||||
|
||||
export interface DocumentTocSidebarProps {
|
||||
width: number
|
||||
disableToc: boolean
|
||||
baseUrl: string
|
||||
}
|
||||
|
||||
export const DocumentTocSidebar: React.FC<DocumentTocSidebarProps> = ({ disableToc, width, baseUrl }) => {
|
||||
const [tocAst, setTocAst] = useState<TocAst>()
|
||||
useExtensionEventEmitterHandler(TableOfContentsMarkdownExtension.EVENT_NAME, setTocAst)
|
||||
return (
|
||||
<div className={`${styles['markdown-document-side']} pt-4`}>
|
||||
<ShowIf condition={!!tocAst && !disableToc}>
|
||||
<WidthBasedTableOfContents tocAst={tocAst as TocAst} baseUrl={baseUrl} width={width} />
|
||||
</ShowIf>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import type { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
|
||||
import { useOnUserScroll } from './use-on-user-scroll'
|
||||
import { useScrollToLineMark } from './use-scroll-to-line-mark'
|
||||
import type { LineMarkerPosition } from '../../../markdown-renderer/extensions/linemarker/types'
|
||||
|
||||
/**
|
||||
* Synchronizes the scroll status of the given container with the given scroll state and posts changes if the user scrolls.
|
||||
*
|
||||
* @param outerContainerRef A reference for the outer container.
|
||||
* @param rendererRef A reference for the renderer
|
||||
* @param numberOfLines The number of lines
|
||||
* @param scrollState The current {@link ScrollState}
|
||||
* @param onScroll A callback that posts new scroll states
|
||||
* @return A tuple of two callbacks.
|
||||
* The first one should be executed if the {@link LineMarkerPosition line marker positions} are updated.
|
||||
* The second one should be executed if the user actually scrolls. Usually it should be attached to the DOM element that the user scrolls.
|
||||
*/
|
||||
export const useDocumentSyncScrolling = (
|
||||
outerContainerRef: React.RefObject<HTMLElement>,
|
||||
rendererRef: React.RefObject<HTMLElement>,
|
||||
numberOfLines: number,
|
||||
scrollState?: ScrollState,
|
||||
onScroll?: (scrollState: ScrollState) => void
|
||||
): [(lineMarkers: LineMarkerPosition[]) => void, React.UIEventHandler<HTMLElement>] => {
|
||||
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 useMemo(() => [onLineMarkerPositionChanged, onUserScroll], [onLineMarkerPositionChanged, onUserScroll])
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import type { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
|
||||
import type { LineMarkerPosition } from '../../../markdown-renderer/extensions/linemarker/types'
|
||||
|
||||
/**
|
||||
* Provides a callback to handle user scrolling.
|
||||
*
|
||||
* @param lineMarks An array of the current {@link LineMarkerPosition LineMarkerPositions}
|
||||
* @param scrollContainer The container to scroll in
|
||||
* @param onScroll A callback that posts new scroll states.
|
||||
* @return A callback that can be called when the user is scrolling.
|
||||
*/
|
||||
export const useOnUserScroll = (
|
||||
lineMarks: LineMarkerPosition[] | undefined,
|
||||
scrollContainer: React.RefObject<HTMLElement>,
|
||||
onScroll: ((newScrollState: ScrollState) => void) | undefined
|
||||
): React.UIEventHandler<HTMLElement> => {
|
||||
return useCallback(() => {
|
||||
if (!scrollContainer.current || !lineMarks || lineMarks.length === 0 || !onScroll) {
|
||||
return
|
||||
}
|
||||
|
||||
const scrollTop = scrollContainer.current.scrollTop
|
||||
|
||||
const lineMarksBeforeScrollTop = lineMarks.filter((lineMark) => lineMark.position <= scrollTop)
|
||||
if (lineMarksBeforeScrollTop.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const lineMarksAfterScrollTop = lineMarks.filter((lineMark) => lineMark.position > scrollTop)
|
||||
if (lineMarksAfterScrollTop.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const beforeLineMark = lineMarksBeforeScrollTop.reduce((prevLineMark, currentLineMark) =>
|
||||
prevLineMark.line >= currentLineMark.line ? prevLineMark : currentLineMark
|
||||
)
|
||||
|
||||
const afterLineMark = lineMarksAfterScrollTop.reduce((prevLineMark, currentLineMark) =>
|
||||
prevLineMark.line < currentLineMark.line ? prevLineMark : currentLineMark
|
||||
)
|
||||
|
||||
const componentHeight = afterLineMark.position - beforeLineMark.position
|
||||
const distanceToBefore = scrollTop - beforeLineMark.position
|
||||
const percentageRaw = distanceToBefore / componentHeight
|
||||
const lineCount = afterLineMark.line - beforeLineMark.line
|
||||
const line = Math.floor(lineCount * percentageRaw + beforeLineMark.line)
|
||||
const lineHeight = componentHeight / lineCount
|
||||
const innerScrolling = Math.floor(((distanceToBefore % lineHeight) / lineHeight) * 100)
|
||||
|
||||
const newScrollState: ScrollState = { firstLineInView: line, scrolledPercentage: innerScrolling }
|
||||
onScroll(newScrollState)
|
||||
}, [lineMarks, onScroll, scrollContainer])
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { RefObject } from 'react'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import type { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
|
||||
import { findLineMarks } from '../../../editor-page/synced-scroll/utils'
|
||||
import type { LineMarkerPosition } from '../../../markdown-renderer/extensions/linemarker/types'
|
||||
|
||||
/**
|
||||
* Scrolls the given container to the correct {@link LineMarkerPosition}.
|
||||
*
|
||||
* @param scrollState The current {@link ScrollState}
|
||||
* @param lineMarks An array of the current {@link LineMarkerPosition LineMarkerPositions}.
|
||||
* @param contentLineCount The number of lines
|
||||
* @param scrollContainer The container to scroll in
|
||||
*/
|
||||
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 (!scrollContainer.current || targetPosition === lastScrollPosition.current) {
|
||||
return
|
||||
}
|
||||
lastScrollPosition.current = targetPosition
|
||||
scrollContainer.current.scrollTo({
|
||||
top: targetPosition
|
||||
})
|
||||
},
|
||||
[scrollContainer]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrollContainer.current || !lineMarks || lineMarks.length === 0 || !scrollState) {
|
||||
return
|
||||
}
|
||||
if (scrollState.firstLineInView < lineMarks[0].line) {
|
||||
scrollTo(0)
|
||||
return
|
||||
}
|
||||
if (scrollState.firstLineInView > lineMarks[lineMarks.length - 1].line) {
|
||||
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 : scrollContainer.current.offsetHeight
|
||||
const lastMarkBeforeLine = lastMarkBefore ? lastMarkBefore.line : 1
|
||||
const firstMarkAfterLine = firstMarkAfter ? firstMarkAfter.line : contentLineCount
|
||||
const linesBetweenMarkers = firstMarkAfterLine - lastMarkBeforeLine
|
||||
const blockHeight = positionAfter - positionBefore
|
||||
const lineHeight = blockHeight / linesBetweenMarkers
|
||||
const position =
|
||||
positionBefore +
|
||||
(scrollState.firstLineInView - lastMarkBeforeLine) * lineHeight +
|
||||
(scrollState.scrolledPercentage / 100) * lineHeight
|
||||
const correctedPosition = Math.floor(position)
|
||||
scrollTo(correctedPosition)
|
||||
}, [contentLineCount, lineMarks, scrollContainer, scrollState, scrollTo])
|
||||
}
|
188
frontend/src/components/render-page/iframe-markdown-renderer.tsx
Normal file
188
frontend/src/components/render-page/iframe-markdown-renderer.tsx
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { ScrollState } from '../editor-page/synced-scroll/scroll-props'
|
||||
import type { BaseConfiguration } from './window-post-message-communicator/rendering-message'
|
||||
import { CommunicationMessageType, RendererType } from './window-post-message-communicator/rendering-message'
|
||||
import { setDarkModePreference } from '../../redux/dark-mode/methods'
|
||||
import { MarkdownDocument } from './markdown-document'
|
||||
import { countWords } from './word-counter'
|
||||
import { useRendererToEditorCommunicator } from '../editor-page/render-context/renderer-to-editor-communicator-context-provider'
|
||||
import { useRendererReceiveHandler } from './window-post-message-communicator/hooks/use-renderer-receive-handler'
|
||||
import { SlideshowMarkdownRenderer } from '../markdown-renderer/slideshow-markdown-renderer'
|
||||
import type { SlideOptions } from '../../redux/note-details/types/slide-show-options'
|
||||
import EventEmitter2 from 'eventemitter2'
|
||||
import { eventEmitterContext } from '../markdown-renderer/hooks/use-extension-event-emitter'
|
||||
|
||||
/**
|
||||
* Wraps the markdown rendering in an iframe.
|
||||
*/
|
||||
export const IframeMarkdownRenderer: React.FC = () => {
|
||||
const [markdownContentLines, setMarkdownContentLines] = useState<string[]>([])
|
||||
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
|
||||
const [baseConfiguration, setBaseConfiguration] = useState<BaseConfiguration | undefined>(undefined)
|
||||
const [slideOptions, setSlideOptions] = useState<SlideOptions>()
|
||||
|
||||
const communicator = useRendererToEditorCommunicator()
|
||||
|
||||
const sendScrolling = useRef<boolean>(false)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.SET_SLIDE_OPTIONS,
|
||||
useCallback((values) => setSlideOptions(values.slideOptions), [])
|
||||
)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.DISABLE_RENDERER_SCROLL_SOURCE,
|
||||
useCallback(() => {
|
||||
sendScrolling.current = false
|
||||
}, [])
|
||||
)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.SET_BASE_CONFIGURATION,
|
||||
useCallback((values) => setBaseConfiguration(values.baseConfiguration), [])
|
||||
)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.SET_MARKDOWN_CONTENT,
|
||||
useCallback((values) => setMarkdownContentLines(values.content), [])
|
||||
)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.SET_DARKMODE,
|
||||
useCallback((values) => setDarkModePreference(values.preference), [])
|
||||
)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.SET_SCROLL_STATE,
|
||||
useCallback((values) => setScrollState(values.scrollState), [])
|
||||
)
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.GET_WORD_COUNT,
|
||||
useCallback(() => {
|
||||
const documentContainer = document.querySelector('[data-word-count-target]')
|
||||
communicator.sendMessageToOtherSide({
|
||||
type: CommunicationMessageType.ON_WORD_COUNT_CALCULATED,
|
||||
words: documentContainer ? countWords(documentContainer) : 0
|
||||
})
|
||||
}, [communicator])
|
||||
)
|
||||
|
||||
const onFirstHeadingChange = useCallback(
|
||||
(firstHeading?: string) => {
|
||||
communicator.sendMessageToOtherSide({
|
||||
type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE,
|
||||
firstHeading
|
||||
})
|
||||
},
|
||||
[communicator]
|
||||
)
|
||||
|
||||
const onMakeScrollSource = useCallback(() => {
|
||||
sendScrolling.current = true
|
||||
communicator.sendMessageToOtherSide({
|
||||
type: CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE
|
||||
})
|
||||
}, [communicator])
|
||||
|
||||
const onScroll = useCallback(
|
||||
(scrollState: ScrollState) => {
|
||||
if (!sendScrolling.current) {
|
||||
return
|
||||
}
|
||||
communicator.sendMessageToOtherSide({
|
||||
type: CommunicationMessageType.SET_SCROLL_STATE,
|
||||
scrollState
|
||||
})
|
||||
},
|
||||
[communicator]
|
||||
)
|
||||
|
||||
const onHeightChange = useCallback(
|
||||
(height: number) => {
|
||||
communicator.sendMessageToOtherSide({
|
||||
type: CommunicationMessageType.ON_HEIGHT_CHANGE,
|
||||
height
|
||||
})
|
||||
},
|
||||
[communicator]
|
||||
)
|
||||
|
||||
const renderer = useMemo(() => {
|
||||
if (!baseConfiguration) {
|
||||
return (
|
||||
<span>
|
||||
This is the render endpoint. If you can read this text then please check your HedgeDoc configuration.
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
switch (baseConfiguration.rendererType) {
|
||||
case RendererType.DOCUMENT:
|
||||
return (
|
||||
<MarkdownDocument
|
||||
additionalOuterContainerClasses={'vh-100 bg-light'}
|
||||
markdownContentLines={markdownContentLines}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onMakeScrollSource={onMakeScrollSource}
|
||||
scrollState={scrollState}
|
||||
onScroll={onScroll}
|
||||
baseUrl={baseConfiguration.baseUrl}
|
||||
onHeightChange={onHeightChange}
|
||||
/>
|
||||
)
|
||||
case RendererType.SLIDESHOW:
|
||||
return (
|
||||
<SlideshowMarkdownRenderer
|
||||
markdownContentLines={markdownContentLines}
|
||||
baseUrl={baseConfiguration.baseUrl}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
scrollState={scrollState}
|
||||
slideOptions={slideOptions}
|
||||
/>
|
||||
)
|
||||
case RendererType.MOTD:
|
||||
case RendererType.INTRO:
|
||||
return (
|
||||
<MarkdownDocument
|
||||
additionalOuterContainerClasses={'vh-100 bg-light overflow-y-hidden'}
|
||||
markdownContentLines={markdownContentLines}
|
||||
baseUrl={baseConfiguration.baseUrl}
|
||||
disableToc={true}
|
||||
onHeightChange={onHeightChange}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}, [
|
||||
baseConfiguration,
|
||||
markdownContentLines,
|
||||
onFirstHeadingChange,
|
||||
onHeightChange,
|
||||
onMakeScrollSource,
|
||||
onScroll,
|
||||
scrollState,
|
||||
slideOptions
|
||||
])
|
||||
|
||||
const extensionEventEmitter = useMemo(() => new EventEmitter2({ wildcard: true }), [])
|
||||
|
||||
useEffect(() => {
|
||||
extensionEventEmitter.onAny((event, values) => {
|
||||
communicator.sendMessageToOtherSide({
|
||||
type: CommunicationMessageType.EXTENSION_EVENT,
|
||||
eventName: typeof event === 'object' ? event.join('.') : event,
|
||||
payload: values
|
||||
})
|
||||
})
|
||||
}, [communicator, extensionEventEmitter])
|
||||
|
||||
return <eventEmitterContext.Provider value={extensionEventEmitter}>{renderer}</eventEmitterContext.Provider>
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.markdown-document {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.markdown-document-side {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.markdown-document-content {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
flex: 0 0 900px;
|
||||
max-width: 100%;
|
||||
width: 900px;
|
||||
}
|
||||
}
|
111
frontend/src/components/render-page/markdown-document.tsx
Normal file
111
frontend/src/components/render-page/markdown-document.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { MutableRefObject } from 'react'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import useResizeObserver from '@react-hook/resize-observer'
|
||||
import { useDocumentSyncScrolling } from './hooks/sync-scroll/use-document-sync-scrolling'
|
||||
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
||||
import { DocumentMarkdownRenderer } from '../markdown-renderer/document-markdown-renderer'
|
||||
import styles from './markdown-document.module.scss'
|
||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||
import { DocumentTocSidebar } from './document-toc-sidebar'
|
||||
|
||||
export interface RendererProps extends ScrollProps {
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
|
||||
markdownContentLines: string[]
|
||||
onHeightChange?: (height: number) => void
|
||||
}
|
||||
|
||||
export interface MarkdownDocumentProps extends RendererProps {
|
||||
additionalOuterContainerClasses?: string
|
||||
additionalRendererClasses?: string
|
||||
disableToc?: boolean
|
||||
baseUrl: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a markdown document and handles scrolling, yaml metadata and a floating table of contents.
|
||||
*
|
||||
* @param additionalOuterContainerClasses Additional classes given to the outer container directly
|
||||
* @param additionalRendererClasses Additional classes given {@link DocumentMarkdownRenderer} directly
|
||||
* @param onFirstHeadingChange The callback to call when the first heading changes.
|
||||
* @param onMakeScrollSource The callback to call if a change of the scroll source is requested-
|
||||
* @param onTaskCheckedChange The callback to call if a task get's checked or unchecked.
|
||||
* @param baseUrl The base url for the renderer
|
||||
* @param markdownContentLines The current content of the markdown document.
|
||||
* @param onImageClick The callback to call if an image is clicked.
|
||||
* @param onScroll The callback to call if the renderer is scrolling.
|
||||
* @param scrollState The current {@link ScrollState}
|
||||
* @param onHeightChange The callback to call if the height of the document changes
|
||||
* @param disableToc If the table of contents should be disabled.
|
||||
* @see https://markdown-it.github.io/
|
||||
*/
|
||||
export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
||||
additionalOuterContainerClasses,
|
||||
additionalRendererClasses,
|
||||
onFirstHeadingChange,
|
||||
onMakeScrollSource,
|
||||
baseUrl,
|
||||
markdownContentLines,
|
||||
onScroll,
|
||||
scrollState,
|
||||
onHeightChange,
|
||||
disableToc
|
||||
}) => {
|
||||
const rendererRef = useRef<HTMLDivElement | null>(null)
|
||||
const [rendererSize, setRendererSize] = useState<DOMRectReadOnly>()
|
||||
useResizeObserver(rendererRef.current, (entry) => {
|
||||
setRendererSize(entry.contentRect)
|
||||
})
|
||||
useEffect(() => onHeightChange?.((rendererSize?.height ?? 0) + 1), [rendererSize, onHeightChange])
|
||||
|
||||
const internalDocumentRenderPaneRef = useRef<HTMLDivElement>(null)
|
||||
const [internalDocumentRenderPaneSize, setInternalDocumentRenderPaneSize] = useState<DOMRectReadOnly>()
|
||||
useResizeObserver(internalDocumentRenderPaneRef.current, (entry) =>
|
||||
setInternalDocumentRenderPaneSize(entry.contentRect)
|
||||
)
|
||||
|
||||
const newlinesAreBreaks = useApplicationState((state) => state.noteDetails.frontmatter.newlinesAreBreaks)
|
||||
|
||||
const contentLineCount = useMemo(() => markdownContentLines.length, [markdownContentLines])
|
||||
const [onLineMarkerPositionChanged, onUserScroll] = useDocumentSyncScrolling(
|
||||
internalDocumentRenderPaneRef,
|
||||
rendererRef,
|
||||
contentLineCount,
|
||||
scrollState,
|
||||
onScroll
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles['markdown-document']} ${additionalOuterContainerClasses ?? ''}`}
|
||||
ref={internalDocumentRenderPaneRef}
|
||||
onScroll={onUserScroll}
|
||||
data-scroll-element={true}
|
||||
onMouseEnter={onMakeScrollSource}
|
||||
onTouchStart={onMakeScrollSource}>
|
||||
<div className={styles['markdown-document-side']} />
|
||||
<div className={styles['markdown-document-content']}>
|
||||
<DocumentMarkdownRenderer
|
||||
outerContainerRef={rendererRef}
|
||||
className={`mb-3 ${additionalRendererClasses ?? ''}`}
|
||||
markdownContentLines={markdownContentLines}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onLineMarkerPositionChanged={onLineMarkerPositionChanged}
|
||||
baseUrl={baseUrl}
|
||||
newlinesAreBreaks={newlinesAreBreaks}
|
||||
/>
|
||||
</div>
|
||||
<DocumentTocSidebar
|
||||
width={internalDocumentRenderPaneSize?.width ?? 0}
|
||||
baseUrl={baseUrl}
|
||||
disableToc={disableToc ?? false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.markdown-toc-sidebar-button {
|
||||
position: fixed;
|
||||
right: 70px;
|
||||
bottom: 30px;
|
||||
|
||||
& > :global(.dropup) {
|
||||
position: sticky;
|
||||
bottom: 20px;
|
||||
right: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import type { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { TableOfContents } from '../../editor-page/table-of-contents/table-of-contents'
|
||||
import styles from './markdown-toc-button.module.scss'
|
||||
|
||||
export interface MarkdownTocButtonProps {
|
||||
tocAst: TocAst
|
||||
baseUrl: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a button that is hovering over the parent and shows a {@link TableOfContents table of contents list} as overlay if clicked.
|
||||
*
|
||||
* @param tocAst the {@link TocAst AST} that should be rendered.
|
||||
* @param baseUrl the base url that will be used to generate the links
|
||||
*/
|
||||
export const TableOfContentsHoveringButton: React.FC<MarkdownTocButtonProps> = ({ tocAst, baseUrl }) => {
|
||||
return (
|
||||
<div className={styles['markdown-toc-sidebar-button']}>
|
||||
<Dropdown drop={'up'}>
|
||||
<Dropdown.Toggle id='toc-overlay-button' variant={'secondary'} className={'no-arrow'}>
|
||||
<ForkAwesomeIcon icon={'list-ol'} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<div className={'p-2'}>
|
||||
<TableOfContents ast={tocAst} baseUrl={baseUrl} />
|
||||
</div>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import type { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { TableOfContents } from '../editor-page/table-of-contents/table-of-contents'
|
||||
import { TableOfContentsHoveringButton } from './markdown-toc-button/table-of-contents-hovering-button'
|
||||
|
||||
export interface DocumentExternalTocProps {
|
||||
tocAst: TocAst
|
||||
width: number
|
||||
baseUrl: string
|
||||
}
|
||||
|
||||
const MAX_WIDTH_FOR_BUTTON_VISIBILITY = 1100
|
||||
|
||||
/**
|
||||
* Renders the {@link TableOfContents table of contents list} for the given {@link TocAst AST}.
|
||||
* If the given width is below {@link MAX_WIDTH_FOR_BUTTON_VISIBILITY the width limit} then a {@link TableOfContentsHoveringButton button} with an overlay will be shown instead.
|
||||
*
|
||||
* @param tocAst the {@link TocAst AST} that should be rendered.
|
||||
* @param width the width that should be used to determine if the button should be shown.
|
||||
* @param baseUrl the base url that will be used to generate the links //TODO: replace with consumer/provider
|
||||
*/
|
||||
export const WidthBasedTableOfContents: React.FC<DocumentExternalTocProps> = ({ tocAst, width, baseUrl }) => {
|
||||
if (width >= MAX_WIDTH_FOR_BUTTON_VISIBILITY) {
|
||||
return <TableOfContents ast={tocAst} className={'sticky'} baseUrl={baseUrl} />
|
||||
} else {
|
||||
return <TableOfContentsHoveringButton tocAst={tocAst} baseUrl={baseUrl} />
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { WindowPostMessageCommunicator } from './window-post-message-communicator'
|
||||
import type {
|
||||
CommunicationMessages,
|
||||
EditorToRendererMessageType,
|
||||
RendererToEditorMessageType
|
||||
} from './rendering-message'
|
||||
import { Logger } from '../../../utils/logger'
|
||||
|
||||
/**
|
||||
* The communicator that is used to send messages from the editor to the renderer.
|
||||
*/
|
||||
export class EditorToRendererCommunicator extends WindowPostMessageCommunicator<
|
||||
RendererToEditorMessageType,
|
||||
EditorToRendererMessageType,
|
||||
CommunicationMessages
|
||||
> {
|
||||
protected createLogger(): Logger {
|
||||
return new Logger('EditorToRendererCommunicator')
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import type { CommunicationMessages, RendererToEditorMessageType } from '../rendering-message'
|
||||
import { useEditorToRendererCommunicator } from '../../../editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
||||
import type { Handler } from '../window-post-message-communicator'
|
||||
|
||||
/**
|
||||
* Sets the handler for the given message type in the current editor to renderer communicator.
|
||||
*
|
||||
* @param messageType The message type that should be used to listen to.
|
||||
* @param handler The handler that should be called if a message with the given message type was received.
|
||||
*/
|
||||
export const useEditorReceiveHandler = <R extends RendererToEditorMessageType>(
|
||||
messageType: R,
|
||||
handler?: Handler<CommunicationMessages, R>
|
||||
): void => {
|
||||
const editorToRendererCommunicator = useEditorToRendererCommunicator()
|
||||
useEffect(() => {
|
||||
if (!handler) {
|
||||
return
|
||||
}
|
||||
editorToRendererCommunicator.on(messageType, handler)
|
||||
return () => editorToRendererCommunicator.off(messageType, handler)
|
||||
}, [editorToRendererCommunicator, handler, messageType])
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
|
||||
/**
|
||||
* Extracts the ready status of the renderer from the global application state.
|
||||
*
|
||||
* @return The current ready status of the renderer.
|
||||
*/
|
||||
export const useIsRendererReady = (): boolean => useApplicationState((state) => state.rendererStatus.rendererReady)
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import type { CommunicationMessages, EditorToRendererMessageType } from '../rendering-message'
|
||||
import type { Handler } from '../window-post-message-communicator'
|
||||
import { useRendererToEditorCommunicator } from '../../../editor-page/render-context/renderer-to-editor-communicator-context-provider'
|
||||
|
||||
export type CommunicationMessageHandler<MESSAGE_TYPE extends EditorToRendererMessageType> = Handler<
|
||||
CommunicationMessages,
|
||||
MESSAGE_TYPE
|
||||
>
|
||||
|
||||
/**
|
||||
* Sets the handler for the given message type in the current renderer to editor communicator.
|
||||
*
|
||||
* @param messageType The message type that should be used to listen to.
|
||||
* @param handler The handler that should be called if a message with the given message type was received.
|
||||
*/
|
||||
export const useRendererReceiveHandler = <MESSAGE_TYPE extends EditorToRendererMessageType>(
|
||||
messageType: MESSAGE_TYPE,
|
||||
handler: CommunicationMessageHandler<MESSAGE_TYPE>
|
||||
): void => {
|
||||
const editorToRendererCommunicator = useRendererToEditorCommunicator()
|
||||
useEffect(() => {
|
||||
editorToRendererCommunicator.on(messageType, handler)
|
||||
return () => editorToRendererCommunicator.off(messageType, handler)
|
||||
}, [editorToRendererCommunicator, handler, messageType])
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import type { CommunicationMessages, EditorToRendererMessageType } from '../rendering-message'
|
||||
import { useEditorToRendererCommunicator } from '../../../editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
||||
import type { MessagePayload } from '../window-post-message-communicator'
|
||||
|
||||
/**
|
||||
* Sends the given message to the renderer.
|
||||
*
|
||||
* @param message The message to send
|
||||
* @param rendererReady Defines if the target renderer is ready
|
||||
*/
|
||||
export const useSendToRenderer = (
|
||||
message: undefined | Extract<CommunicationMessages, MessagePayload<EditorToRendererMessageType>>,
|
||||
rendererReady: boolean
|
||||
): void => {
|
||||
const iframeCommunicator = useEditorToRendererCommunicator()
|
||||
|
||||
useEffect(() => {
|
||||
if (message && rendererReady) {
|
||||
iframeCommunicator.sendMessageToOtherSide(message)
|
||||
}
|
||||
}, [iframeCommunicator, message, rendererReady])
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { WindowPostMessageCommunicator } from './window-post-message-communicator'
|
||||
import type {
|
||||
CommunicationMessages,
|
||||
EditorToRendererMessageType,
|
||||
RendererToEditorMessageType
|
||||
} from './rendering-message'
|
||||
import { Logger } from '../../../utils/logger'
|
||||
|
||||
/**
|
||||
* The communicator that is used to send messages from the renderer to the editor.
|
||||
*/
|
||||
export class RendererToEditorCommunicator extends WindowPostMessageCommunicator<
|
||||
EditorToRendererMessageType,
|
||||
RendererToEditorMessageType,
|
||||
CommunicationMessages
|
||||
> {
|
||||
protected createLogger(): Logger {
|
||||
return new Logger('RendererToEditorCommunicator')
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ScrollState } from '../../editor-page/synced-scroll/scroll-props'
|
||||
import type { SlideOptions } from '../../../redux/note-details/types/slide-show-options'
|
||||
import type { DarkModePreference } from '../../../redux/dark-mode/types'
|
||||
|
||||
export enum CommunicationMessageType {
|
||||
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
||||
RENDERER_READY = 'RENDERER_READY',
|
||||
SET_DARKMODE = 'SET_DARKMODE',
|
||||
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
|
||||
ENABLE_RENDERER_SCROLL_SOURCE = 'ENABLE_RENDERER_SCROLL_SOURCE',
|
||||
DISABLE_RENDERER_SCROLL_SOURCE = 'DISABLE_RENDERER_SCROLL_SOURCE',
|
||||
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
||||
ON_HEIGHT_CHANGE = 'ON_HEIGHT_CHANGE',
|
||||
SET_BASE_CONFIGURATION = 'SET_BASE_CONFIGURATION',
|
||||
GET_WORD_COUNT = 'GET_WORD_COUNT',
|
||||
ON_WORD_COUNT_CALCULATED = 'ON_WORD_COUNT_CALCULATED',
|
||||
SET_SLIDE_OPTIONS = 'SET_SLIDE_OPTIONS',
|
||||
IMAGE_UPLOAD = 'IMAGE_UPLOAD',
|
||||
EXTENSION_EVENT = 'EXTENSION_EVENT'
|
||||
}
|
||||
|
||||
export interface NoPayloadMessage<TYPE extends CommunicationMessageType> {
|
||||
type: TYPE
|
||||
}
|
||||
|
||||
export interface SetDarkModeMessage {
|
||||
type: CommunicationMessageType.SET_DARKMODE
|
||||
preference: DarkModePreference
|
||||
}
|
||||
|
||||
export interface ExtensionEvent {
|
||||
type: CommunicationMessageType.EXTENSION_EVENT
|
||||
eventName: string
|
||||
payload: unknown
|
||||
}
|
||||
|
||||
export interface ImageDetails {
|
||||
alt?: string
|
||||
src: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface ImageUploadMessage {
|
||||
type: CommunicationMessageType.IMAGE_UPLOAD
|
||||
dataUri: string
|
||||
fileName: string
|
||||
lineIndex?: number
|
||||
placeholderIndexInLine?: number
|
||||
}
|
||||
|
||||
export interface SetBaseUrlMessage {
|
||||
type: CommunicationMessageType.SET_BASE_CONFIGURATION
|
||||
baseConfiguration: BaseConfiguration
|
||||
}
|
||||
|
||||
export interface GetWordCountMessage {
|
||||
type: CommunicationMessageType.GET_WORD_COUNT
|
||||
}
|
||||
|
||||
export interface SetMarkdownContentMessage {
|
||||
type: CommunicationMessageType.SET_MARKDOWN_CONTENT
|
||||
content: string[]
|
||||
}
|
||||
|
||||
export interface SetScrollStateMessage {
|
||||
type: CommunicationMessageType.SET_SCROLL_STATE
|
||||
scrollState: ScrollState
|
||||
}
|
||||
|
||||
export interface OnFirstHeadingChangeMessage {
|
||||
type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE
|
||||
firstHeading: string | undefined
|
||||
}
|
||||
|
||||
export interface SetSlideOptionsMessage {
|
||||
type: CommunicationMessageType.SET_SLIDE_OPTIONS
|
||||
slideOptions: SlideOptions
|
||||
}
|
||||
|
||||
export interface OnHeightChangeMessage {
|
||||
type: CommunicationMessageType.ON_HEIGHT_CHANGE
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface OnWordCountCalculatedMessage {
|
||||
type: CommunicationMessageType.ON_WORD_COUNT_CALCULATED
|
||||
words: number
|
||||
}
|
||||
|
||||
export type CommunicationMessages =
|
||||
| NoPayloadMessage<CommunicationMessageType.RENDERER_READY>
|
||||
| NoPayloadMessage<CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE>
|
||||
| NoPayloadMessage<CommunicationMessageType.DISABLE_RENDERER_SCROLL_SOURCE>
|
||||
| SetDarkModeMessage
|
||||
| SetBaseUrlMessage
|
||||
| GetWordCountMessage
|
||||
| SetMarkdownContentMessage
|
||||
| SetScrollStateMessage
|
||||
| OnFirstHeadingChangeMessage
|
||||
| SetSlideOptionsMessage
|
||||
| OnHeightChangeMessage
|
||||
| OnWordCountCalculatedMessage
|
||||
| ImageUploadMessage
|
||||
| ExtensionEvent
|
||||
|
||||
export type EditorToRendererMessageType =
|
||||
| CommunicationMessageType.SET_MARKDOWN_CONTENT
|
||||
| CommunicationMessageType.SET_DARKMODE
|
||||
| CommunicationMessageType.SET_SCROLL_STATE
|
||||
| CommunicationMessageType.SET_BASE_CONFIGURATION
|
||||
| CommunicationMessageType.GET_WORD_COUNT
|
||||
| CommunicationMessageType.SET_SLIDE_OPTIONS
|
||||
| CommunicationMessageType.DISABLE_RENDERER_SCROLL_SOURCE
|
||||
|
||||
export type RendererToEditorMessageType =
|
||||
| CommunicationMessageType.RENDERER_READY
|
||||
| CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE
|
||||
| CommunicationMessageType.ON_FIRST_HEADING_CHANGE
|
||||
| CommunicationMessageType.SET_SCROLL_STATE
|
||||
| CommunicationMessageType.ON_HEIGHT_CHANGE
|
||||
| CommunicationMessageType.ON_WORD_COUNT_CALCULATED
|
||||
| CommunicationMessageType.IMAGE_UPLOAD
|
||||
| CommunicationMessageType.EXTENSION_EVENT
|
||||
|
||||
export enum RendererType {
|
||||
DOCUMENT = 'document',
|
||||
INTRO = 'intro',
|
||||
SLIDESHOW = 'slideshow',
|
||||
MOTD = 'motd'
|
||||
}
|
||||
|
||||
export interface BaseConfiguration {
|
||||
baseUrl: string
|
||||
rendererType: RendererType
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Logger } from '../../../utils/logger'
|
||||
import { Optional } from '@mrdrogdrog/optional'
|
||||
import EventEmitter2 from 'eventemitter2'
|
||||
|
||||
/**
|
||||
* Error that will be thrown if a message couldn't be sent.
|
||||
*/
|
||||
export class IframeCommunicatorSendingError extends Error {}
|
||||
|
||||
export type Handler<MESSAGES, MESSAGE_TYPE extends string> = (
|
||||
values: Extract<MESSAGES, MessagePayload<MESSAGE_TYPE>>
|
||||
) => void
|
||||
|
||||
export interface MessagePayloadWithUuid<MESSAGE_TYPE extends string> {
|
||||
uuid: string
|
||||
payload: MessagePayload<MESSAGE_TYPE>
|
||||
}
|
||||
|
||||
export interface MessagePayload<MESSAGE_TYPE extends string> {
|
||||
type: MESSAGE_TYPE
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for communication between renderer and editor.
|
||||
*/
|
||||
export abstract class WindowPostMessageCommunicator<
|
||||
RECEIVE_TYPE extends string,
|
||||
SEND_TYPE extends string,
|
||||
MESSAGES extends MessagePayload<RECEIVE_TYPE | SEND_TYPE>
|
||||
> {
|
||||
private messageTarget?: Window
|
||||
private communicationEnabled: boolean
|
||||
private readonly emitter: EventEmitter2 = new EventEmitter2()
|
||||
private readonly log: Logger
|
||||
private readonly boundListener: (event: MessageEvent) => void
|
||||
|
||||
public constructor(private readonly uuid: string, private readonly targetOrigin: string) {
|
||||
this.boundListener = this.handleEvent.bind(this)
|
||||
this.communicationEnabled = false
|
||||
this.log = this.createLogger()
|
||||
}
|
||||
|
||||
public getUuid(): string {
|
||||
return this.uuid
|
||||
}
|
||||
|
||||
protected abstract createLogger(): Logger
|
||||
|
||||
/**
|
||||
* Registers the event listener on the current global {@link window}.
|
||||
*/
|
||||
public registerEventListener(): void {
|
||||
window.addEventListener('message', this.boundListener, { passive: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the message event listener from the {@link window}.
|
||||
*/
|
||||
public unregisterEventListener(): void {
|
||||
window.removeEventListener('message', this.boundListener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the target for message sending.
|
||||
* Messages can be sent as soon as the communication is enabled.
|
||||
*
|
||||
* @param otherSide The target {@link Window} that should receive the messages.
|
||||
* @see enableCommunication
|
||||
*/
|
||||
public setMessageTarget(otherSide: Window): void {
|
||||
this.messageTarget = otherSide
|
||||
this.communicationEnabled = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsets the message target. Should be used if the old target isn't available anymore.
|
||||
*/
|
||||
public unsetMessageTarget(): void {
|
||||
this.messageTarget = undefined
|
||||
this.communicationEnabled = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the message communication.
|
||||
* Should be called as soon as the other sides is ready to receive messages.
|
||||
*/
|
||||
public enableCommunication(): void {
|
||||
this.communicationEnabled = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the message target.
|
||||
*
|
||||
* @param message The message to send.
|
||||
*/
|
||||
public sendMessageToOtherSide(message: Extract<MESSAGES, MessagePayload<SEND_TYPE>>): void {
|
||||
if (this.messageTarget === undefined || this.targetOrigin === undefined) {
|
||||
throw new IframeCommunicatorSendingError(`Other side is not set.\nMessage was: ${JSON.stringify(message)}`)
|
||||
}
|
||||
if (!this.communicationEnabled) {
|
||||
throw new IframeCommunicatorSendingError(
|
||||
`Communication isn't enabled. Maybe the other side is not ready?\nMessage was: ${JSON.stringify(message)}`
|
||||
)
|
||||
}
|
||||
this.log.debug('Sent event', message)
|
||||
this.messageTarget.postMessage(
|
||||
{
|
||||
uuid: this.uuid,
|
||||
payload: message
|
||||
} as MessagePayloadWithUuid<SEND_TYPE>,
|
||||
this.targetOrigin
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler for the given message type.
|
||||
*
|
||||
* @param messageType The message type for which the handler should be called
|
||||
* @param handler The handler that processes messages with the given message type.
|
||||
*/
|
||||
public on<R extends RECEIVE_TYPE>(messageType: R, handler: Handler<MESSAGES, R>): void {
|
||||
this.log.debug('Set handler for', messageType)
|
||||
this.emitter.on(messageType, handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a handler for the given message type.
|
||||
*
|
||||
* @param messageType The message type for which the handler should be removed
|
||||
* @param handler The handler that should be removed.
|
||||
*/
|
||||
public off<R extends RECEIVE_TYPE>(messageType: R, handler: Handler<MESSAGES, R>): void {
|
||||
this.log.debug('Unset handler for', messageType)
|
||||
this.emitter.off(messageType, handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives the message events and calls the handler that is mapped to the correct type.
|
||||
*
|
||||
* @param event The received event
|
||||
* @return {@link true} if the event was processed.
|
||||
*/
|
||||
protected handleEvent(event: MessageEvent<MessagePayloadWithUuid<RECEIVE_TYPE>>): void {
|
||||
if (event.origin !== this.targetOrigin) {
|
||||
this.log.error(
|
||||
`message declined. origin was "${event.origin}" but expected "${String(this.targetOrigin)}"`,
|
||||
event.data
|
||||
)
|
||||
return
|
||||
}
|
||||
Optional.ofNullable(event.data)
|
||||
.filter((value) => value.uuid === this.uuid)
|
||||
.ifPresent((payload) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
this.emitter.emit(payload.payload.type, payload.payload)
|
||||
})
|
||||
}
|
||||
}
|
53
frontend/src/components/render-page/word-counter.ts
Normal file
53
frontend/src/components/render-page/word-counter.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import wordsCount from 'words-count'
|
||||
|
||||
/** List of HTML tag names that should not be counted. */
|
||||
const EXCLUDED_TAGS = ['img', 'pre', 'nav']
|
||||
/** List of class names that should not be counted. */
|
||||
const EXCLUDED_CLASSES = ['katex-mathml']
|
||||
|
||||
/**
|
||||
* Checks whether the given node is an excluded HTML tag and therefore should be
|
||||
* excluded from counting.
|
||||
*
|
||||
* @param node The node to test.
|
||||
* @return {@link true} if the node should be excluded, {@link false} otherwise.
|
||||
*/
|
||||
const isExcludedTag = (node: Element | ChildNode): boolean => {
|
||||
return EXCLUDED_TAGS.includes(node.nodeName.toLowerCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given node is a HTML element with an excluded class name,
|
||||
* so that it should be excluded.
|
||||
*
|
||||
* @param node The node to test.
|
||||
* @return {@link true} if the node should be excluded, {@link false} otherwise.
|
||||
*/
|
||||
const isExcludedClass = (node: Element | ChildNode): boolean => {
|
||||
return EXCLUDED_CLASSES.some((excludedClass) => (node as HTMLElement).classList?.contains(excludedClass))
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the words of the given node while ignoring empty nodes and excluded
|
||||
* nodes. Child nodes will recursively be counted as well.
|
||||
*
|
||||
* @param node The node whose content's words should be counted.
|
||||
* @return The number of words counted in this node and its children.
|
||||
*/
|
||||
export const countWords = (node: Element | ChildNode): number => {
|
||||
if (!node.textContent || isExcludedTag(node) || isExcludedClass(node)) {
|
||||
return 0
|
||||
}
|
||||
if (!node.hasChildNodes()) {
|
||||
return wordsCount(node.textContent)
|
||||
}
|
||||
return [...node.childNodes].reduce((words, childNode) => {
|
||||
return words + countWords(childNode)
|
||||
}, 0)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue