mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-24 10:46:30 -05:00
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:
parent
d726e6d94e
commit
8a1b29fddc
14 changed files with 123 additions and 8 deletions
|
@ -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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'}
|
||||||
|
|
|
@ -7,3 +7,9 @@
|
||||||
.frame {
|
.frame {
|
||||||
color-scheme: initial;
|
color-scheme: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.frame {
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
15
frontend/src/components/editor-page/print.scss
Normal file
15
frontend/src/components/editor-page/print.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
16
frontend/src/components/editor-page/utils/print-iframe.ts
Normal file
16
frontend/src/components/editor-page/utils/print-iframe.ts
Normal 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()
|
||||||
|
}
|
|
@ -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}
|
||||||
|
|
|
@ -27,3 +27,10 @@
|
||||||
width: 900px;
|
width: 900px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.document {
|
||||||
|
height: auto !important;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue