mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2026-08-07 07:14:49 +00:00
feat(slide-mode): add print mode for slides
When clicking the print button in the sidebar or executing the print keyboard shortcut, we toggle to reveal.js' print mode in order to print all slides correctly. After printing, the mode is toggled back. Since this might hang under certain circumstances, there is a delay and fallback to always return back to the normal view even if the afterprint event didn't fire correctly. Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
@@ -9,6 +9,8 @@ import { useCallback } from 'react'
|
|||||||
import { setPrintMode } from '../../../redux/print-mode/methods'
|
import { setPrintMode } from '../../../redux/print-mode/methods'
|
||||||
import { useEditorToRendererCommunicator } from '../render-context/editor-to-renderer-communicator-context-provider'
|
import { useEditorToRendererCommunicator } from '../render-context/editor-to-renderer-communicator-context-provider'
|
||||||
import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message'
|
import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message'
|
||||||
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
|
import { NoteType } from '@hedgedoc/commons'
|
||||||
|
|
||||||
const TIMEOUT_BEFORE_PRINT = 25
|
const TIMEOUT_BEFORE_PRINT = 25
|
||||||
|
|
||||||
@@ -18,9 +20,18 @@ const TIMEOUT_BEFORE_PRINT = 25
|
|||||||
export const usePrintIframe = (): (() => void) => {
|
export const usePrintIframe = (): (() => void) => {
|
||||||
const iframeCommunicator = useEditorToRendererCommunicator()
|
const iframeCommunicator = useEditorToRendererCommunicator()
|
||||||
const rendererReady = useIsRendererReady()
|
const rendererReady = useIsRendererReady()
|
||||||
|
const isSlideDeck = useApplicationState((state) => state.noteDetails?.frontmatter.type === NoteType.SLIDE)
|
||||||
|
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
if (!rendererReady) {
|
if (!rendererReady || !iframeCommunicator) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSlideDeck) {
|
||||||
|
iframeCommunicator.sendMessageToOtherSide({
|
||||||
|
type: CommunicationMessageType.SET_PRINT_MODE,
|
||||||
|
printMode: true
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +50,7 @@ export const usePrintIframe = (): (() => void) => {
|
|||||||
printMode: false
|
printMode: false
|
||||||
})
|
})
|
||||||
}, TIMEOUT_BEFORE_PRINT)
|
}, TIMEOUT_BEFORE_PRINT)
|
||||||
}, [rendererReady, iframeCommunicator])
|
}, [iframeCommunicator, isSlideDeck, rendererReady])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,6 +61,9 @@ export const usePrintIframe = (): (() => void) => {
|
|||||||
export const usePrintSelf = () => {
|
export const usePrintSelf = () => {
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
setPrintMode(true)
|
setPrintMode(true)
|
||||||
|
if (document.querySelector('.reveal')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.print()
|
window.print()
|
||||||
setPrintMode(false)
|
setPrintMode(false)
|
||||||
|
|||||||
@@ -4,11 +4,18 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { Logger } from '../../../utils/logger'
|
import { Logger } from '../../../utils/logger'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import type Reveal from 'reveal.js'
|
import type Reveal from 'reveal.js'
|
||||||
import type { RevealOptions } from 'reveal.js'
|
import type { RevealOptions } from 'reveal.js'
|
||||||
|
|
||||||
const log = new Logger('reveal.js')
|
const log = new Logger('reveal.js')
|
||||||
|
const printDelay = 100
|
||||||
|
|
||||||
|
const cleanupRevealPrintMode = (): void => {
|
||||||
|
document.documentElement.classList.remove('reveal-print', 'print-pdf')
|
||||||
|
document.body.style.removeProperty('width')
|
||||||
|
document.body.style.removeProperty('height')
|
||||||
|
}
|
||||||
|
|
||||||
export enum REVEAL_STATUS {
|
export enum REVEAL_STATUS {
|
||||||
NOT_INITIALISED,
|
NOT_INITIALISED,
|
||||||
@@ -31,17 +38,22 @@ const initialSlideState: SlideState = {
|
|||||||
* @param markdownContentLines An array of markdown lines.
|
* @param markdownContentLines An array of markdown lines.
|
||||||
* @param slideOptions The slide options.
|
* @param slideOptions The slide options.
|
||||||
* @param targetSlideState The slide that should be shown.
|
* @param targetSlideState The slide that should be shown.
|
||||||
|
* @param printMode Defines if the presentation should be printed.
|
||||||
|
* @param onPrintModeConsumed Callback to reset print mode after printing.
|
||||||
* @return The current state of reveal.js
|
* @return The current state of reveal.js
|
||||||
* @see https://revealjs.com/
|
* @see https://revealjs.com/
|
||||||
*/
|
*/
|
||||||
export const useReveal = (
|
export const useReveal = (
|
||||||
markdownContentLines: string[],
|
markdownContentLines: string[],
|
||||||
slideOptions?: RevealOptions,
|
slideOptions?: RevealOptions,
|
||||||
targetSlideState?: SlideState
|
targetSlideState?: SlideState,
|
||||||
|
printMode = false,
|
||||||
|
onPrintModeConsumed?: () => void
|
||||||
): REVEAL_STATUS => {
|
): REVEAL_STATUS => {
|
||||||
const [deck, setDeck] = useState<Reveal>()
|
const [deck, setDeck] = useState<Reveal>()
|
||||||
const [revealStatus, setRevealStatus] = useState<REVEAL_STATUS>(REVEAL_STATUS.NOT_INITIALISED)
|
const [revealStatus, setRevealStatus] = useState<REVEAL_STATUS>(REVEAL_STATUS.NOT_INITIALISED)
|
||||||
const currentSlideState = useRef<SlideState>(initialSlideState)
|
const currentSlideState = useRef<SlideState>(initialSlideState)
|
||||||
|
const printInProgress = useRef<boolean>(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (revealStatus !== REVEAL_STATUS.NOT_INITIALISED) {
|
if (revealStatus !== REVEAL_STATUS.NOT_INITIALISED) {
|
||||||
@@ -49,10 +61,11 @@ export const useReveal = (
|
|||||||
}
|
}
|
||||||
setRevealStatus(REVEAL_STATUS.INITIALISING)
|
setRevealStatus(REVEAL_STATUS.INITIALISING)
|
||||||
log.debug('Initialize with slide options', slideOptions)
|
log.debug('Initialize with slide options', slideOptions)
|
||||||
|
const revealOptions: RevealOptions = printMode ? { ...slideOptions, view: 'print' } : (slideOptions ?? {})
|
||||||
|
|
||||||
import(/* webpackChunkName: "reveal" */ 'reveal.js')
|
import(/* webpackChunkName: "reveal" */ 'reveal.js')
|
||||||
.then((revealImport) => {
|
.then((revealImport) => {
|
||||||
const reveal = new revealImport.default({})
|
const reveal = new revealImport.default(revealOptions)
|
||||||
reveal
|
reveal
|
||||||
.initialize()
|
.initialize()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -76,20 +89,34 @@ export const useReveal = (
|
|||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
log.error('Error while loading reveal.js', error)
|
log.error('Error while loading reveal.js', error)
|
||||||
})
|
})
|
||||||
}, [revealStatus, slideOptions])
|
}, [printMode, revealStatus, slideOptions])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!deck || revealStatus !== REVEAL_STATUS.INITIALISED) {
|
if (!deck) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
deck.destroy()
|
||||||
|
if (printMode) {
|
||||||
|
cleanupRevealPrintMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [deck, printMode])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!deck || printMode || revealStatus !== REVEAL_STATUS.INITIALISED) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.debug('Sync deck')
|
log.debug('Sync deck')
|
||||||
deck.sync()
|
deck.sync()
|
||||||
deck.slide(currentSlideState.current.indexHorizontal, currentSlideState.current.indexVertical)
|
deck.slide(currentSlideState.current.indexHorizontal, currentSlideState.current.indexVertical)
|
||||||
}, [markdownContentLines, deck, revealStatus])
|
}, [markdownContentLines, deck, printMode, revealStatus])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!deck ||
|
!deck ||
|
||||||
|
printMode ||
|
||||||
slideOptions === undefined ||
|
slideOptions === undefined ||
|
||||||
Object.keys(slideOptions).length === 0 ||
|
Object.keys(slideOptions).length === 0 ||
|
||||||
revealStatus !== REVEAL_STATUS.INITIALISED
|
revealStatus !== REVEAL_STATUS.INITIALISED
|
||||||
@@ -98,14 +125,44 @@ export const useReveal = (
|
|||||||
}
|
}
|
||||||
log.debug('Apply config', slideOptions)
|
log.debug('Apply config', slideOptions)
|
||||||
deck.configure(slideOptions)
|
deck.configure(slideOptions)
|
||||||
}, [deck, revealStatus, slideOptions])
|
}, [deck, printMode, revealStatus, slideOptions])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!deck || targetSlideState === undefined || revealStatus !== REVEAL_STATUS.INITIALISED) {
|
if (!deck || printMode || targetSlideState === undefined || revealStatus !== REVEAL_STATUS.INITIALISED) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
deck.slide(targetSlideState.indexHorizontal, targetSlideState.indexVertical)
|
deck.slide(targetSlideState.indexHorizontal, targetSlideState.indexVertical)
|
||||||
}, [deck, revealStatus, targetSlideState])
|
}, [deck, printMode, revealStatus, targetSlideState])
|
||||||
|
|
||||||
|
const printPresentation = useCallback(async () => {
|
||||||
|
if (!deck || revealStatus !== REVEAL_STATUS.INITIALISED || printInProgress.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
printInProgress.current = true
|
||||||
|
|
||||||
|
const finishPrint = (): void => {
|
||||||
|
window.removeEventListener('afterprint', finishPrint)
|
||||||
|
cleanupRevealPrintMode()
|
||||||
|
onPrintModeConsumed?.()
|
||||||
|
printInProgress.current = false
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
window.addEventListener('afterprint', finishPrint)
|
||||||
|
window.print()
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Error while printing reveal.js presentation', error)
|
||||||
|
finishPrint()
|
||||||
|
}
|
||||||
|
}, printDelay)
|
||||||
|
}, [deck, onPrintModeConsumed, revealStatus])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (printMode) {
|
||||||
|
void printPresentation()
|
||||||
|
}
|
||||||
|
}, [printMode, printPresentation])
|
||||||
|
|
||||||
return revealStatus
|
return revealStatus
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { EventEmitter2 } from 'eventemitter2'
|
|||||||
import React, { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { setPrintMode } from '../../redux/print-mode/methods'
|
import { setPrintMode } from '../../redux/print-mode/methods'
|
||||||
import { usePrintSelfKeyboardShortcut } from '../editor-page/hooks/use-print-keyboard-shortcut'
|
import { usePrintSelfKeyboardShortcut } from '../editor-page/hooks/use-print-keyboard-shortcut'
|
||||||
|
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps the markdown rendering in an iframe.
|
* Wraps the markdown rendering in an iframe.
|
||||||
@@ -33,6 +34,9 @@ export const RenderPageContent: React.FC = () => {
|
|||||||
const sendScrolling = useRef<boolean>(false)
|
const sendScrolling = useRef<boolean>(false)
|
||||||
const [newLinesAreBreaks, setNewLinesAreBreaks] = useState<boolean>(true)
|
const [newLinesAreBreaks, setNewLinesAreBreaks] = useState<boolean>(true)
|
||||||
const [slideOptions, setSlideOptions] = useState<RevealOptions>()
|
const [slideOptions, setSlideOptions] = useState<RevealOptions>()
|
||||||
|
const printMode = useApplicationState((state) => state.printMode)
|
||||||
|
|
||||||
|
const resetPrintMode = useCallback(() => setPrintMode(false), [])
|
||||||
|
|
||||||
useRendererReceiveHandler(
|
useRendererReceiveHandler(
|
||||||
CommunicationMessageType.SET_SLIDE_OPTIONS,
|
CommunicationMessageType.SET_SLIDE_OPTIONS,
|
||||||
@@ -189,6 +193,8 @@ export const RenderPageContent: React.FC = () => {
|
|||||||
newLinesAreBreaks={newLinesAreBreaks}
|
newLinesAreBreaks={newLinesAreBreaks}
|
||||||
scrollState={scrollState}
|
scrollState={scrollState}
|
||||||
slideOptions={slideOptions}
|
slideOptions={slideOptions}
|
||||||
|
printMode={printMode}
|
||||||
|
onPrintModeConsumed={resetPrintMode}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case RendererType.SIMPLE:
|
case RendererType.SIMPLE:
|
||||||
@@ -210,6 +216,8 @@ export const RenderPageContent: React.FC = () => {
|
|||||||
onHeightChange,
|
onHeightChange,
|
||||||
onMakeScrollSource,
|
onMakeScrollSource,
|
||||||
onScroll,
|
onScroll,
|
||||||
|
printMode,
|
||||||
|
resetPrintMode,
|
||||||
scrollState,
|
scrollState,
|
||||||
slideOptions
|
slideOptions
|
||||||
])
|
])
|
||||||
|
|||||||
+17
-4
@@ -18,6 +18,12 @@ import React, { useMemo, useRef } from 'react'
|
|||||||
|
|
||||||
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps, Pick<ScrollProps, 'scrollState'> {
|
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps, Pick<ScrollProps, 'scrollState'> {
|
||||||
slideOptions?: RevealOptions
|
slideOptions?: RevealOptions
|
||||||
|
printMode?: boolean
|
||||||
|
onPrintModeConsumed?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type SlideshowMarkdownRendererContentProps = SlideshowMarkdownRendererProps & {
|
||||||
|
printMode: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,11 +35,18 @@ export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererPr
|
|||||||
* @param newLinesAreBreaks If newlines are rendered as breaks or not
|
* @param newLinesAreBreaks If newlines are rendered as breaks or not
|
||||||
*/
|
*/
|
||||||
export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps> = ({
|
export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps> = ({
|
||||||
|
printMode = false,
|
||||||
|
...props
|
||||||
|
}) => <SlideshowMarkdownRendererContent key={printMode ? 'print' : 'preview'} printMode={printMode} {...props} />
|
||||||
|
|
||||||
|
const SlideshowMarkdownRendererContent: React.FC<SlideshowMarkdownRendererContentProps> = ({
|
||||||
markdownContentLines,
|
markdownContentLines,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
newLinesAreBreaks,
|
newLinesAreBreaks,
|
||||||
scrollState,
|
scrollState,
|
||||||
slideOptions
|
slideOptions,
|
||||||
|
printMode,
|
||||||
|
onPrintModeConsumed
|
||||||
}) => {
|
}) => {
|
||||||
const markdownBodyRef = useRef<HTMLDivElement>(null)
|
const markdownBodyRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@@ -47,11 +60,11 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps>
|
|||||||
() => (scrollState ? findSlideForLine(markdownContentLines, scrollState.firstLineInView) : undefined),
|
() => (scrollState ? findSlideForLine(markdownContentLines, scrollState.firstLineInView) : undefined),
|
||||||
[markdownContentLines, scrollState]
|
[markdownContentLines, scrollState]
|
||||||
)
|
)
|
||||||
const revealStatus = useReveal(markdownContentLines, slideOptions, targetSlideState)
|
const revealStatus = useReveal(markdownContentLines, slideOptions, targetSlideState, printMode, onPrintModeConsumed)
|
||||||
|
|
||||||
const slideShowDOM = useMemo(
|
const slideShowDOM = useMemo(
|
||||||
() =>
|
() =>
|
||||||
revealStatus === REVEAL_STATUS.INITIALISED ? (
|
revealStatus === REVEAL_STATUS.INITIALISED || printMode ? (
|
||||||
<MarkdownToReact
|
<MarkdownToReact
|
||||||
markdownContentLines={markdownContentLines}
|
markdownContentLines={markdownContentLines}
|
||||||
markdownRenderExtensions={extensions}
|
markdownRenderExtensions={extensions}
|
||||||
@@ -61,7 +74,7 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps>
|
|||||||
) : (
|
) : (
|
||||||
<LoadingSlide />
|
<LoadingSlide />
|
||||||
),
|
),
|
||||||
[extensions, markdownContentLines, newLinesAreBreaks, revealStatus]
|
[extensions, markdownContentLines, newLinesAreBreaks, printMode, revealStatus]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ declare module 'reveal.js' {
|
|||||||
transitionSpeed?: string | undefined
|
transitionSpeed?: string | undefined
|
||||||
backgroundTransition?: string | undefined
|
backgroundTransition?: string | undefined
|
||||||
viewDistance?: number | undefined
|
viewDistance?: number | undefined
|
||||||
|
view?: 'print' | 'reader' | 'scroll' | undefined
|
||||||
|
|
||||||
// https://github.com/hakimel/reveal.js/#parallax-background
|
// https://github.com/hakimel/reveal.js/#parallax-background
|
||||||
// Parallax background image
|
// Parallax background image
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { printModeActionsCreator, printModeReducer } from './slice'
|
||||||
|
|
||||||
|
describe('printModeReducer', () => {
|
||||||
|
it('updates the print mode', () => {
|
||||||
|
expect(printModeReducer(false, printModeActionsCreator.setPrintMode(true))).toBe(true)
|
||||||
|
expect(printModeReducer(true, printModeActionsCreator.setPrintMode(false))).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user