mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-29 23:05:00 -05:00
Extract toc button (#1302)
* Extract toc button Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
4e9b059d2e
commit
4720f2d36b
10 changed files with 99 additions and 148 deletions
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
* 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'
|
|
||||||
|
|
||||||
export const useAdaptedLineMarkerCallback = (
|
|
||||||
documentRenderPaneRef: RefObject<HTMLDivElement> | undefined,
|
|
||||||
rendererRef: RefObject<HTMLDivElement>,
|
|
||||||
onLineMarkerPositionChanged: ((lineMarkerPosition: LineMarkerPosition[]) => void) | undefined
|
|
||||||
): ((lineMarkerPosition: LineMarkerPosition[]) => void) => {
|
|
||||||
return useCallback(
|
|
||||||
(linkMarkerPositions) => {
|
|
||||||
if (
|
|
||||||
!onLineMarkerPositionChanged ||
|
|
||||||
!documentRenderPaneRef ||
|
|
||||||
!documentRenderPaneRef.current ||
|
|
||||||
!rendererRef.current
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const documentRenderPaneTop = documentRenderPaneRef.current.offsetTop ?? 0
|
|
||||||
const rendererTop = rendererRef.current.offsetTop ?? 0
|
|
||||||
const offset = rendererTop - documentRenderPaneTop
|
|
||||||
onLineMarkerPositionChanged(
|
|
||||||
linkMarkerPositions.map((oldMarker) => ({
|
|
||||||
line: oldMarker.line,
|
|
||||||
position: oldMarker.position + offset
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[documentRenderPaneRef, onLineMarkerPositionChanged, rendererRef]
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
* 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])
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
* 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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -164,7 +164,7 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({
|
||||||
)
|
)
|
||||||
|
|
||||||
const onCursorActivity = useCallback(
|
const onCursorActivity = useCallback(
|
||||||
(editorWithActivity) => {
|
(editorWithActivity: Editor) => {
|
||||||
setStatusBarInfo(createStatusInfo(editorWithActivity, maxLength))
|
setStatusBarInfo(createStatusInfo(editorWithActivity, maxLength))
|
||||||
},
|
},
|
||||||
[maxLength]
|
[maxLength]
|
||||||
|
|
|
@ -62,15 +62,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-toc-sidebar-button {
|
|
||||||
position: fixed;
|
|
||||||
right: 70px;
|
|
||||||
bottom: 30px;
|
|
||||||
|
|
||||||
& > .dropup {
|
|
||||||
position: sticky;
|
|
||||||
bottom: 20px;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -44,7 +44,6 @@ export const IntroPage: React.FC = () => {
|
||||||
<RenderIframe
|
<RenderIframe
|
||||||
frameClasses={'w-100 overflow-y-hidden'}
|
frameClasses={'w-100 overflow-y-hidden'}
|
||||||
markdownContent={introPageContent as string}
|
markdownContent={introPageContent as string}
|
||||||
disableToc={true}
|
|
||||||
onRendererReadyChange={setRendererReady}
|
onRendererReadyChange={setRendererReady}
|
||||||
rendererType={RendererType.INTRO}
|
rendererType={RendererType.INTRO}
|
||||||
forcedDarkMode={true}
|
forcedDarkMode={true}
|
||||||
|
|
|
@ -6,20 +6,18 @@
|
||||||
|
|
||||||
import { TocAst } from 'markdown-it-toc-done-right'
|
import { TocAst } from 'markdown-it-toc-done-right'
|
||||||
import React, { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Dropdown } from 'react-bootstrap'
|
|
||||||
import useResizeObserver from 'use-resize-observer'
|
import useResizeObserver from 'use-resize-observer'
|
||||||
import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
|
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
|
||||||
import { NoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
import { NoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
||||||
import { YamlArrayDeprecationAlert } from '../editor-page/renderer-pane/yaml-array-deprecation-alert'
|
import { YamlArrayDeprecationAlert } from '../editor-page/renderer-pane/yaml-array-deprecation-alert'
|
||||||
import { useSyncedScrolling } from '../editor-page/synced-scroll/hooks/use-synced-scrolling'
|
import { useSyncedScrolling } from '../editor-page/synced-scroll/hooks/use-synced-scrolling'
|
||||||
import { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
import { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import { TableOfContents } from '../editor-page/table-of-contents/table-of-contents'
|
|
||||||
import { BasicMarkdownRenderer } from '../markdown-renderer/basic-markdown-renderer'
|
import { BasicMarkdownRenderer } from '../markdown-renderer/basic-markdown-renderer'
|
||||||
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
||||||
import './markdown-document.scss'
|
import './markdown-document.scss'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { ApplicationState } from '../../redux'
|
import { ApplicationState } from '../../redux'
|
||||||
|
import { WidthBasedTableOfContents } from './width-based-table-of-contents'
|
||||||
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
|
|
||||||
export interface RendererProps extends ScrollProps {
|
export interface RendererProps extends ScrollProps {
|
||||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||||
|
@ -27,15 +25,15 @@ export interface RendererProps extends ScrollProps {
|
||||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||||
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
|
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
|
||||||
markdownContent: string
|
markdownContent: string
|
||||||
baseUrl?: string
|
|
||||||
onImageClick?: ImageClickHandler
|
onImageClick?: ImageClickHandler
|
||||||
onHeightChange?: (height: number) => void
|
onHeightChange?: (height: number) => void
|
||||||
disableToc?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarkdownDocumentProps extends RendererProps {
|
export interface MarkdownDocumentProps extends RendererProps {
|
||||||
additionalOuterContainerClasses?: string
|
additionalOuterContainerClasses?: string
|
||||||
additionalRendererClasses?: string
|
additionalRendererClasses?: string
|
||||||
|
disableToc?: boolean
|
||||||
|
baseUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
||||||
|
@ -105,23 +103,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className={'markdown-document-side pt-4'}>
|
<div className={'markdown-document-side pt-4'}>
|
||||||
<ShowIf condition={!!tocAst && !disableToc}>
|
<ShowIf condition={!!tocAst && !disableToc}>
|
||||||
<ShowIf condition={containerWidth >= 1100}>
|
<WidthBasedTableOfContents tocAst={tocAst as TocAst} baseUrl={baseUrl} width={containerWidth} />
|
||||||
<TableOfContents ast={tocAst as TocAst} className={'sticky'} baseUrl={baseUrl} />
|
|
||||||
</ShowIf>
|
|
||||||
<ShowIf condition={containerWidth < 1100}>
|
|
||||||
<div className={'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 as TocAst} baseUrl={baseUrl} />
|
|
||||||
</div>
|
|
||||||
</Dropdown.Menu>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</ShowIf>
|
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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;
|
||||||
|
|
||||||
|
& > .dropup {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { Dropdown } from 'react-bootstrap'
|
||||||
|
import { 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 './markdown-toc-button.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
|
||||||
|
* @return the created component
|
||||||
|
*/
|
||||||
|
export const TableOfContentsHoveringButton: React.FC<MarkdownTocButtonProps> = ({ tocAst, baseUrl }) => {
|
||||||
|
return (
|
||||||
|
<div className={'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>
|
||||||
|
)
|
||||||
|
}
|
35
src/components/render-page/width-based-table-of-contents.tsx
Normal file
35
src/components/render-page/width-based-table-of-contents.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { 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
|
||||||
|
* @return the created component
|
||||||
|
*/
|
||||||
|
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} />
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue