feat(frontend): add basic print functionality

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2024-11-01 20:21:16 +01:00
parent d726e6d94e
commit 8a1b29fddc
No known key found for this signature in database
GPG key ID: DB99ADDDC5C0AF82
14 changed files with 123 additions and 8 deletions

View file

@ -971,5 +971,11 @@
"example": "```markdown=12\nline1\n```\n```markdown=+\nline2\n```\n```markdown=\nline3\n```" "example": "```markdown=12\nline1\n```\n```markdown=+\nline2\n```\n```markdown=\nline3\n```"
} }
} }
},
"print": {
"warning": {
"title": "Warning!",
"text": "To print this note, please use the print button in the export menu in the sidebar. Printing this page directly will not work as expected."
}
} }
} }

View file

@ -177,11 +177,14 @@ export const RendererIframe: React.FC<RendererIframeProps> = ({
<Fragment> <Fragment>
{!rendererReady && showWaitSpinner && <WaitSpinner />} {!rendererReady && showWaitSpinner && <WaitSpinner />}
<iframe <iframe
id={'editor-renderer-iframe'}
style={{ height: `${frameHeight}px` }} style={{ height: `${frameHeight}px` }}
{...cypressId('documentIframe')} {...cypressId('documentIframe')}
onLoad={onIframeLoad} onLoad={onIframeLoad}
title='render' title='render'
{...(isTestMode ? {} : { sandbox: 'allow-downloads allow-same-origin allow-scripts allow-popups' })} {...(isTestMode
? {}
: { sandbox: 'allow-downloads allow-same-origin allow-scripts allow-popups allow-modals' })}
allowFullScreen={true} allowFullScreen={true}
ref={frameReference} ref={frameReference}
referrerPolicy={'no-referrer'} referrerPolicy={'no-referrer'}

View file

@ -7,3 +7,9 @@
.frame { .frame {
color-scheme: initial; color-scheme: initial;
} }
@media print {
.frame {
height: auto !important;
}
}

View file

@ -15,8 +15,11 @@ import { useUpdateLocalHistoryEntry } from './hooks/use-update-local-history-ent
import { RendererPane } from './renderer-pane/renderer-pane' import { RendererPane } from './renderer-pane/renderer-pane'
import { Sidebar } from './sidebar/sidebar' import { Sidebar } from './sidebar/sidebar'
import { Splitter } from './splitter/splitter' import { Splitter } from './splitter/splitter'
import { PrintWarning } from './print-warning/print-warning'
import React, { useMemo, useRef } from 'react' import React, { useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import './print.scss'
import { usePrintKeyboardShortcut } from './hooks/use-print-keyboard-shortcut'
export enum ScrollSource { export enum ScrollSource {
EDITOR = 'editor', EDITOR = 'editor',
@ -28,7 +31,7 @@ export enum ScrollSource {
*/ */
export const EditorPageContent: React.FC = () => { export const EditorPageContent: React.FC = () => {
useTranslation() useTranslation()
usePrintKeyboardShortcut()
useUpdateLocalHistoryEntry() useUpdateLocalHistoryEntry()
const scrollSource = useRef<ScrollSource>(ScrollSource.EDITOR) const scrollSource = useRef<ScrollSource>(ScrollSource.EDITOR)
@ -68,6 +71,7 @@ export const EditorPageContent: React.FC = () => {
<ExtensionEventEmitterProvider> <ExtensionEventEmitterProvider>
{editorExtensionComponents} {editorExtensionComponents}
<CommunicatorImageLightbox /> <CommunicatorImageLightbox />
<PrintWarning />
<div className={'flex-fill d-flex h-100 w-100 overflow-hidden flex-row'}> <div className={'flex-fill d-flex h-100 w-100 overflow-hidden flex-row'}>
<Splitter <Splitter
left={leftPane} left={leftPane}

View file

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { printIframe } from '../utils/print-iframe'
import { useEffect } from 'react'
/**
* Hook to listen for the print keyboard shortcut and print the content of the renderer iframe.
*/
export const usePrintKeyboardShortcut = (): void => {
useEffect(() => {
const handlePrint = (event: KeyboardEvent): void => {
if (event.key === 'p' && (event.ctrlKey || event.metaKey)) {
event.preventDefault()
printIframe()
}
}
window.addEventListener('keydown', handlePrint)
return () => {
window.removeEventListener('keydown', handlePrint)
}
}, [])
}

View file

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { Alert } from 'react-bootstrap'
import { Trans } from 'react-i18next'
export const PrintWarning: React.FC = () => {
return (
<div className={'d-none d-print-block'}>
<Alert variant={'warning'}>
<Alert.Heading>
<Trans i18nKey={'print.warning.title'} />
</Alert.Heading>
<p>
<Trans i18nKey={'print.warning.text'} />
</p>
</Alert>
</div>
)
}

View file

@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@media print {
body {
& > div.d-flex {
nav, #editor-edit-pane, #editor-splitter, #editor-sidebar, #editor-view-pane {
display: none;
}
}
}
}

View file

@ -43,7 +43,7 @@ export const Sidebar: React.FC = () => {
const selectionIsNotNone = selectedMenu !== DocumentSidebarMenuSelection.NONE const selectionIsNotNone = selectedMenu !== DocumentSidebarMenuSelection.NONE
return ( return (
<div className={styles['slide-sidebar']}> <div className={styles['slide-sidebar']} id={'editor-sidebar'}>
<div ref={sideBarRef} className={`${styles['sidebar-inner']} ${selectionIsNotNone ? styles['show'] : ''}`}> <div ref={sideBarRef} className={`${styles['sidebar-inner']} ${selectionIsNotNone ? styles['show'] : ''}`}>
<UsersOnlineSidebarMenu <UsersOnlineSidebarMenu
menuId={DocumentSidebarMenuSelection.USERS_ONLINE} menuId={DocumentSidebarMenuSelection.USERS_ONLINE}

View file

@ -8,13 +8,14 @@ import { SidebarButton } from '../../../sidebar-button/sidebar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { PrinterFill as IconPrinterFill } from 'react-bootstrap-icons' import { PrinterFill as IconPrinterFill } from 'react-bootstrap-icons'
import { Trans } from 'react-i18next' import { Trans } from 'react-i18next'
import { printIframe } from '../../../../utils/print-iframe'
/** /**
* Editor sidebar entry for exporting the markdown content into a local file. * Editor sidebar entry for exporting the markdown content into a local file.
*/ */
export const ExportPrintSidebarEntry: React.FC = () => { export const ExportPrintSidebarEntry: React.FC = () => {
const onClick = useCallback(() => { const onClick = useCallback(() => {
window.print() printIframe()
}, []) }, [])
return ( return (

View file

@ -60,7 +60,7 @@ export const SplitDivider: React.FC<SplitDividerProps> = ({
}, [dividerButtonsShift, forceOpen]) }, [dividerButtonsShift, forceOpen])
return ( return (
<div className={styles.divider} {...testId('splitter-divider')}> <div className={styles.divider} {...testId('splitter-divider')} id={'editor-splitter'}>
<div className={className}> <div className={className}>
<div className={styles.buttons}> <div className={styles.buttons}>
<Button variant={focusLeft ? 'secondary' : 'light'} onClick={onLeftButtonClick}> <Button variant={focusLeft ? 'secondary' : 'light'} onClick={onLeftButtonClick}>

View file

@ -150,7 +150,10 @@ export const Splitter: React.FC<SplitterProps> = ({ additionalContainerClassName
onTouchEnd={onStopResizing} onTouchEnd={onStopResizing}
onMouseUp={onStopResizing}></div> onMouseUp={onStopResizing}></div>
)} )}
<div className={styles['left']} style={{ width: `calc(${adjustedRelativeSplitValue}% - 5px)` }}> <div
id={'editor-edit-pane'}
className={styles['left']}
style={{ width: `calc(${adjustedRelativeSplitValue}% - 5px)` }}>
<div className={styles['inner']}>{left}</div> <div className={styles['inner']}>{left}</div>
</div> </div>
<SplitDivider <SplitDivider
@ -162,7 +165,10 @@ export const Splitter: React.FC<SplitterProps> = ({ additionalContainerClassName
focusRight={relativeSplitValue > 100 - SNAP_PERCENTAGE} focusRight={relativeSplitValue > 100 - SNAP_PERCENTAGE}
dividerButtonsShift={dividerButtonsShift} dividerButtonsShift={dividerButtonsShift}
/> />
<div className={styles['right']} style={{ width: `calc(100% - ${adjustedRelativeSplitValue}%)` }}> <div
id={'editor-view-pane'}
className={styles['right']}
style={{ width: `calc(100% - ${adjustedRelativeSplitValue}%)` }}>
<div className={styles['inner']}>{right}</div> <div className={styles['inner']}>{right}</div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Prints the content of the renderer iframe.
*/
export const printIframe = (): void => {
const iframe = document.getElementById('editor-renderer-iframe') as HTMLIFrameElement
if (!iframe) {
return
}
iframe.contentWindow.print()
}

View file

@ -76,7 +76,7 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
return ( return (
<div <div
className={`${styles.document} vh-100`} className={`vh-100 ${styles.document}`}
ref={internalDocumentRenderPaneRef} ref={internalDocumentRenderPaneRef}
onScroll={onUserScroll} onScroll={onUserScroll}
data-scroll-element={true} data-scroll-element={true}

View file

@ -27,3 +27,10 @@
width: 900px; width: 900px;
} }
} }
@media print {
.document {
height: auto !important;
color: #000;
}
}