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 { useEditorToRendererCommunicator } from '../render-context/editor-to-renderer-communicator-context-provider'
|
||||
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
|
||||
|
||||
@@ -18,9 +20,18 @@ const TIMEOUT_BEFORE_PRINT = 25
|
||||
export const usePrintIframe = (): (() => void) => {
|
||||
const iframeCommunicator = useEditorToRendererCommunicator()
|
||||
const rendererReady = useIsRendererReady()
|
||||
const isSlideDeck = useApplicationState((state) => state.noteDetails?.frontmatter.type === NoteType.SLIDE)
|
||||
|
||||
return useCallback(() => {
|
||||
if (!rendererReady) {
|
||||
if (!rendererReady || !iframeCommunicator) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isSlideDeck) {
|
||||
iframeCommunicator.sendMessageToOtherSide({
|
||||
type: CommunicationMessageType.SET_PRINT_MODE,
|
||||
printMode: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,7 +50,7 @@ export const usePrintIframe = (): (() => void) => {
|
||||
printMode: false
|
||||
})
|
||||
}, TIMEOUT_BEFORE_PRINT)
|
||||
}, [rendererReady, iframeCommunicator])
|
||||
}, [iframeCommunicator, isSlideDeck, rendererReady])
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,6 +61,9 @@ export const usePrintIframe = (): (() => void) => {
|
||||
export const usePrintSelf = () => {
|
||||
return useCallback(() => {
|
||||
setPrintMode(true)
|
||||
if (document.querySelector('.reveal')) {
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
window.print()
|
||||
setPrintMode(false)
|
||||
|
||||
@@ -4,11 +4,18 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
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 { RevealOptions } from '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 {
|
||||
NOT_INITIALISED,
|
||||
@@ -31,17 +38,22 @@ const initialSlideState: SlideState = {
|
||||
* @param markdownContentLines An array of markdown lines.
|
||||
* @param slideOptions The slide options.
|
||||
* @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
|
||||
* @see https://revealjs.com/
|
||||
*/
|
||||
export const useReveal = (
|
||||
markdownContentLines: string[],
|
||||
slideOptions?: RevealOptions,
|
||||
targetSlideState?: SlideState
|
||||
targetSlideState?: SlideState,
|
||||
printMode = false,
|
||||
onPrintModeConsumed?: () => void
|
||||
): REVEAL_STATUS => {
|
||||
const [deck, setDeck] = useState<Reveal>()
|
||||
const [revealStatus, setRevealStatus] = useState<REVEAL_STATUS>(REVEAL_STATUS.NOT_INITIALISED)
|
||||
const currentSlideState = useRef<SlideState>(initialSlideState)
|
||||
const printInProgress = useRef<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (revealStatus !== REVEAL_STATUS.NOT_INITIALISED) {
|
||||
@@ -49,10 +61,11 @@ export const useReveal = (
|
||||
}
|
||||
setRevealStatus(REVEAL_STATUS.INITIALISING)
|
||||
log.debug('Initialize with slide options', slideOptions)
|
||||
const revealOptions: RevealOptions = printMode ? { ...slideOptions, view: 'print' } : (slideOptions ?? {})
|
||||
|
||||
import(/* webpackChunkName: "reveal" */ 'reveal.js')
|
||||
.then((revealImport) => {
|
||||
const reveal = new revealImport.default({})
|
||||
const reveal = new revealImport.default(revealOptions)
|
||||
reveal
|
||||
.initialize()
|
||||
.then(() => {
|
||||
@@ -76,20 +89,34 @@ export const useReveal = (
|
||||
.catch((error: Error) => {
|
||||
log.error('Error while loading reveal.js', error)
|
||||
})
|
||||
}, [revealStatus, slideOptions])
|
||||
}, [printMode, revealStatus, slideOptions])
|
||||
|
||||
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
|
||||
}
|
||||
log.debug('Sync deck')
|
||||
deck.sync()
|
||||
deck.slide(currentSlideState.current.indexHorizontal, currentSlideState.current.indexVertical)
|
||||
}, [markdownContentLines, deck, revealStatus])
|
||||
}, [markdownContentLines, deck, printMode, revealStatus])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!deck ||
|
||||
printMode ||
|
||||
slideOptions === undefined ||
|
||||
Object.keys(slideOptions).length === 0 ||
|
||||
revealStatus !== REVEAL_STATUS.INITIALISED
|
||||
@@ -98,14 +125,44 @@ export const useReveal = (
|
||||
}
|
||||
log.debug('Apply config', slideOptions)
|
||||
deck.configure(slideOptions)
|
||||
}, [deck, revealStatus, slideOptions])
|
||||
}, [deck, printMode, revealStatus, slideOptions])
|
||||
|
||||
useEffect(() => {
|
||||
if (!deck || targetSlideState === undefined || revealStatus !== REVEAL_STATUS.INITIALISED) {
|
||||
if (!deck || printMode || targetSlideState === undefined || revealStatus !== REVEAL_STATUS.INITIALISED) {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { EventEmitter2 } from 'eventemitter2'
|
||||
import React, { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { setPrintMode } from '../../redux/print-mode/methods'
|
||||
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.
|
||||
@@ -33,6 +34,9 @@ export const RenderPageContent: React.FC = () => {
|
||||
const sendScrolling = useRef<boolean>(false)
|
||||
const [newLinesAreBreaks, setNewLinesAreBreaks] = useState<boolean>(true)
|
||||
const [slideOptions, setSlideOptions] = useState<RevealOptions>()
|
||||
const printMode = useApplicationState((state) => state.printMode)
|
||||
|
||||
const resetPrintMode = useCallback(() => setPrintMode(false), [])
|
||||
|
||||
useRendererReceiveHandler(
|
||||
CommunicationMessageType.SET_SLIDE_OPTIONS,
|
||||
@@ -189,6 +193,8 @@ export const RenderPageContent: React.FC = () => {
|
||||
newLinesAreBreaks={newLinesAreBreaks}
|
||||
scrollState={scrollState}
|
||||
slideOptions={slideOptions}
|
||||
printMode={printMode}
|
||||
onPrintModeConsumed={resetPrintMode}
|
||||
/>
|
||||
)
|
||||
case RendererType.SIMPLE:
|
||||
@@ -210,6 +216,8 @@ export const RenderPageContent: React.FC = () => {
|
||||
onHeightChange,
|
||||
onMakeScrollSource,
|
||||
onScroll,
|
||||
printMode,
|
||||
resetPrintMode,
|
||||
scrollState,
|
||||
slideOptions
|
||||
])
|
||||
|
||||
+17
-4
@@ -18,6 +18,12 @@ import React, { useMemo, useRef } from 'react'
|
||||
|
||||
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps, Pick<ScrollProps, 'scrollState'> {
|
||||
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
|
||||
*/
|
||||
export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps> = ({
|
||||
printMode = false,
|
||||
...props
|
||||
}) => <SlideshowMarkdownRendererContent key={printMode ? 'print' : 'preview'} printMode={printMode} {...props} />
|
||||
|
||||
const SlideshowMarkdownRendererContent: React.FC<SlideshowMarkdownRendererContentProps> = ({
|
||||
markdownContentLines,
|
||||
baseUrl,
|
||||
newLinesAreBreaks,
|
||||
scrollState,
|
||||
slideOptions
|
||||
slideOptions,
|
||||
printMode,
|
||||
onPrintModeConsumed
|
||||
}) => {
|
||||
const markdownBodyRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -47,11 +60,11 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps>
|
||||
() => (scrollState ? findSlideForLine(markdownContentLines, scrollState.firstLineInView) : undefined),
|
||||
[markdownContentLines, scrollState]
|
||||
)
|
||||
const revealStatus = useReveal(markdownContentLines, slideOptions, targetSlideState)
|
||||
const revealStatus = useReveal(markdownContentLines, slideOptions, targetSlideState, printMode, onPrintModeConsumed)
|
||||
|
||||
const slideShowDOM = useMemo(
|
||||
() =>
|
||||
revealStatus === REVEAL_STATUS.INITIALISED ? (
|
||||
revealStatus === REVEAL_STATUS.INITIALISED || printMode ? (
|
||||
<MarkdownToReact
|
||||
markdownContentLines={markdownContentLines}
|
||||
markdownRenderExtensions={extensions}
|
||||
@@ -61,7 +74,7 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps>
|
||||
) : (
|
||||
<LoadingSlide />
|
||||
),
|
||||
[extensions, markdownContentLines, newLinesAreBreaks, revealStatus]
|
||||
[extensions, markdownContentLines, newLinesAreBreaks, printMode, revealStatus]
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -37,6 +37,7 @@ declare module 'reveal.js' {
|
||||
transitionSpeed?: string | undefined
|
||||
backgroundTransition?: string | undefined
|
||||
viewDistance?: number | undefined
|
||||
view?: 'print' | 'reader' | 'scroll' | undefined
|
||||
|
||||
// https://github.com/hakimel/reveal.js/#parallax-background
|
||||
// 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