feat(frontend): add basic print functionality

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Erik Michelson 2024-11-01 20:21:16 +01:00 committed by Philip Molares
parent 09e365ceea
commit c5dd5eac0d
51 changed files with 520 additions and 80 deletions

View file

@ -107,12 +107,6 @@
font-size: 1em; font-size: 1em;
} }
hr {
box-sizing: initial;
height: 0;
overflow: visible;
}
input { input {
font: inherit; font: inherit;
margin: 0; margin: 0;

View file

@ -17,6 +17,7 @@
@import "./github-markdown"; @import "./github-markdown";
@import "./markdown-tweaks"; @import "./markdown-tweaks";
@import "./reveal"; @import "./reveal";
@import "./print";
.text-black, body[data-bs-theme=dark] .text-black { .text-black, body[data-bs-theme=dark] .text-black {
color: $black; color: $black;

View file

@ -1,5 +1,5 @@
/* /*!
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View file

@ -0,0 +1,73 @@
/*!
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@media print {
.heading-anchor, .footnote-backref {
display: none;
}
a[href] {
border-bottom: none;
text-decoration: none;
&::after {
content: ' (' attr(href) ')';
font-size: 0.75em;
}
}
nav.table-of-contents, sup.footnote-ref {
a[href]::after {
display: none;
content: '';
}
}
sup.footnote-ref {
a[href] {
color: unset;
}
}
abbr[title] {
border-bottom: none !important;
text-decoration: none !important;
}
mark {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
h1, h2, h3, h4, h5 {
break-after: avoid;
page-break-after: avoid;
}
table, figure, p, img, ul, ol, pre, code {
break-inside: avoid;
page-break-inside: avoid;
}
@page {
padding: 1.5cm;
margin: 1cm auto;
}
.print-only {
display: inline;
}
hr {
background-color: transparent !important;
border-bottom: 2px solid #bbbcbf;
}
}
@media screen {
.print-only {
display: none;
}
}

View file

@ -370,7 +370,8 @@
} }
}, },
"rawHtml": "Raw HTML", "rawHtml": "Raw HTML",
"markdown-file": "Markdown file" "markdown-file": "Markdown file",
"print": "Print"
}, },
"import": { "import": {
"clipboard": "Clipboard", "clipboard": "Clipboard",
@ -971,5 +972,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

@ -3,7 +3,7 @@
exports[`Copy to clipboard button show an error text if clipboard api isn't available 1`] = ` exports[`Copy to clipboard button show an error text if clipboard api isn't available 1`] = `
<div> <div>
<button <button
class="btn btn-dark btn-sm" class="copy-button btn btn-dark btn-sm"
title="renderer.highlightCode.copyCode" title="renderer.highlightCode.copyCode"
type="button" type="button"
> >
@ -16,7 +16,7 @@ exports[`Copy to clipboard button show an error text if clipboard api isn't avai
<div> <div>
<button <button
aria-describedby="copied_35a35a31-c259-48c4-b75a-8da99859dcdb" aria-describedby="copied_35a35a31-c259-48c4-b75a-8da99859dcdb"
class="btn btn-dark btn-sm" class="copy-button btn btn-dark btn-sm"
title="renderer.highlightCode.copyCode" title="renderer.highlightCode.copyCode"
type="button" type="button"
> >
@ -28,7 +28,7 @@ exports[`Copy to clipboard button show an error text if clipboard api isn't avai
exports[`Copy to clipboard button shows an error text if writing failed 1`] = ` exports[`Copy to clipboard button shows an error text if writing failed 1`] = `
<div> <div>
<button <button
class="btn btn-dark btn-sm" class="copy-button btn btn-dark btn-sm"
title="renderer.highlightCode.copyCode" title="renderer.highlightCode.copyCode"
type="button" type="button"
> >
@ -41,7 +41,7 @@ exports[`Copy to clipboard button shows an error text if writing failed 2`] = `
<div> <div>
<button <button
aria-describedby="copied_35a35a31-c259-48c4-b75a-8da99859dcdb" aria-describedby="copied_35a35a31-c259-48c4-b75a-8da99859dcdb"
class="btn btn-dark btn-sm" class="copy-button btn btn-dark btn-sm"
title="renderer.highlightCode.copyCode" title="renderer.highlightCode.copyCode"
type="button" type="button"
> >
@ -53,7 +53,7 @@ exports[`Copy to clipboard button shows an error text if writing failed 2`] = `
exports[`Copy to clipboard button shows an success text if writing succeeded 1`] = ` exports[`Copy to clipboard button shows an success text if writing succeeded 1`] = `
<div> <div>
<button <button
class="btn btn-dark btn-sm" class="copy-button btn btn-dark btn-sm"
title="renderer.highlightCode.copyCode" title="renderer.highlightCode.copyCode"
type="button" type="button"
> >
@ -66,7 +66,7 @@ exports[`Copy to clipboard button shows an success text if writing succeeded 2`]
<div> <div>
<button <button
aria-describedby="copied_35a35a31-c259-48c4-b75a-8da99859dcdb" aria-describedby="copied_35a35a31-c259-48c4-b75a-8da99859dcdb"
class="btn btn-dark btn-sm" class="copy-button btn btn-dark btn-sm"
title="renderer.highlightCode.copyCode" title="renderer.highlightCode.copyCode"
type="button" type="button"
> >

View file

@ -12,6 +12,7 @@ import React, { Fragment, useRef } from 'react'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import { Files as IconFiles } from 'react-bootstrap-icons' import { Files as IconFiles } from 'react-bootstrap-icons'
import type { Variant } from 'react-bootstrap/types' import type { Variant } from 'react-bootstrap/types'
import styles from './style.module.scss'
export interface CopyToClipboardButtonProps extends PropsWithDataCypressId { export interface CopyToClipboardButtonProps extends PropsWithDataCypressId {
content: string content: string
@ -40,6 +41,7 @@ export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({
return ( return (
<Fragment> <Fragment>
<Button <Button
className={styles['copy-button']}
ref={button} ref={button}
size={size} size={size}
variant={variant} variant={variant}

View file

@ -0,0 +1,11 @@
/*!
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@media print {
.copy-button {
display: none;
}
}

View file

@ -55,3 +55,10 @@
} }
} }
} }
@media print {
.code-highlighter {
border: black solid 1px;
border-radius: var(--bs-border-radius);
}
}

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -27,7 +27,9 @@ export const useForceRenderPageUrlOnIframeLoadCallback = (
const forcedUrl = useMemo(() => { const forcedUrl = useMemo(() => {
const renderUrl = new URL(rendererBaseUrl) const renderUrl = new URL(rendererBaseUrl)
renderUrl.pathname += 'render' renderUrl.pathname += 'render'
renderUrl.searchParams.set('uuid', iframeCommunicator.getUuid()) if (iframeCommunicator !== undefined) {
renderUrl.searchParams.set('uuid', iframeCommunicator.getUuid())
}
return renderUrl.toString() return renderUrl.toString()
}, [iframeCommunicator, rendererBaseUrl]) }, [iframeCommunicator, rendererBaseUrl])
const redirectionInProgress = useRef<boolean>(false) const redirectionInProgress = useRef<boolean>(false)

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -66,7 +66,7 @@ export const RendererIframe: React.FC<RendererIframeProps> = ({
const [rendererReady, setRendererReady] = useState<boolean>(false) const [rendererReady, setRendererReady] = useState<boolean>(false)
const frameReference = useRef<HTMLIFrameElement>(null) const frameReference = useRef<HTMLIFrameElement>(null)
const iframeCommunicator = useEditorToRendererCommunicator() const iframeCommunicator = useEditorToRendererCommunicator()
const log = useMemo(() => new Logger(`RendererIframe[${iframeCommunicator.getUuid()}]`), [iframeCommunicator]) const log = useMemo(() => new Logger(`RendererIframe[${iframeCommunicator?.getUuid()}]`), [iframeCommunicator])
const resetRendererReady = useCallback(() => { const resetRendererReady = useCallback(() => {
log.debug('Reset render status') log.debug('Reset render status')
@ -97,7 +97,7 @@ export const RendererIframe: React.FC<RendererIframeProps> = ({
useEffect(() => { useEffect(() => {
if (!rendererReady) { if (!rendererReady) {
iframeCommunicator.unsetMessageTarget() iframeCommunicator?.unsetMessageTarget()
} }
}, [iframeCommunicator, rendererReady]) }, [iframeCommunicator, rendererReady])
@ -141,9 +141,9 @@ export const RendererIframe: React.FC<RendererIframeProps> = ({
log.error('Load triggered without content window') log.error('Load triggered without content window')
return return
} }
iframeCommunicator.setMessageTarget(otherWindow) iframeCommunicator?.setMessageTarget(otherWindow)
iframeCommunicator.enableCommunication() iframeCommunicator?.enableCommunication()
iframeCommunicator.sendMessageToOtherSide({ iframeCommunicator?.sendMessageToOtherSide({
type: CommunicationMessageType.SET_BASE_CONFIGURATION, type: CommunicationMessageType.SET_BASE_CONFIGURATION,
baseConfiguration: { baseConfiguration: {
baseUrl: window.location.toString(), baseUrl: window.location.toString(),
@ -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,38 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { usePrintIframe, usePrintSelf } from '../utils/print-iframe'
import { useCallback, useEffect, useMemo } from 'react'
/**
* Hook to listen for the print keyboard shortcut and print the content of the renderer iframe.
*/
export const usePrintKeyboardShortcut = (): void => {
const printCallbackOutside = usePrintIframe()
const printCallbackInside = usePrintSelf()
const isIframe = useMemo(() => window.top !== window.self, [])
const handlePrint = useCallback(
(event: KeyboardEvent): void => {
if (event.key === 'p' && (event.ctrlKey || event.metaKey)) {
event.preventDefault()
if (isIframe) {
printCallbackInside()
} else {
printCallbackOutside()
}
}
},
[isIframe, printCallbackInside, printCallbackOutside]
)
useEffect(() => {
window.addEventListener('keydown', handlePrint)
return () => {
window.removeEventListener('keydown', handlePrint)
}
}, [handlePrint])
}

View file

@ -0,0 +1,27 @@
/*
* 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'
/**
* Renders a warning when the user tries to open the print dialog from the browser.
*/
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

@ -1,7 +1,7 @@
'use client' 'use client'
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -10,7 +10,9 @@ import { EditorToRendererCommunicator } from '../../render-page/window-post-mess
import type { PropsWithChildren } from 'react' import type { PropsWithChildren } from 'react'
import React, { createContext, useContext, useEffect, useMemo } from 'react' import React, { createContext, useContext, useEffect, useMemo } from 'react'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { Logger } from '../../../utils/logger'
const logger = new Logger('EditorToRendererCommunicator')
const EditorToRendererCommunicatorContext = createContext<EditorToRendererCommunicator | undefined>(undefined) const EditorToRendererCommunicatorContext = createContext<EditorToRendererCommunicator | undefined>(undefined)
/** /**
@ -19,10 +21,11 @@ const EditorToRendererCommunicatorContext = createContext<EditorToRendererCommun
* @return the received communicator * @return the received communicator
* @throws {Error} if no communicator was received * @throws {Error} if no communicator was received
*/ */
export const useEditorToRendererCommunicator: () => EditorToRendererCommunicator = () => { export const useEditorToRendererCommunicator = (): EditorToRendererCommunicator | undefined => {
const communicatorFromContext = useContext(EditorToRendererCommunicatorContext) const communicatorFromContext = useContext(EditorToRendererCommunicatorContext)
if (!communicatorFromContext) { if (!communicatorFromContext) {
throw new Error('No editor-to-renderer-iframe-communicator received. Did you forget to use the provider component?') logger.error('No editor-to-renderer-iframe-communicator received. Did you forget to use the provider component?')
return undefined
} }
return communicatorFromContext return communicatorFromContext
} }

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

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { cypressId } from '../../../../../../utils/cypress-attribute'
import { SidebarButton } from '../../../sidebar-button/sidebar-button'
import React from 'react'
import { PrinterFill as IconPrinterFill } from 'react-bootstrap-icons'
import { Trans } from 'react-i18next'
import { usePrintIframe } from '../../../../utils/print-iframe'
/**
* Editor sidebar entry for exporting the markdown content into a local file.
*/
export const ExportPrintSidebarEntry: React.FC = () => {
const printIframe = usePrintIframe()
return (
<SidebarButton {...cypressId('menu-export-print')} onClick={printIframe} icon={IconPrinterFill}>
<Trans i18nKey={'editor.export.print'} />
</SidebarButton>
)
}

View file

@ -20,6 +20,7 @@ import { concatCssClasses } from '../../../../../utils/concat-css-classes'
import styles from '../../sidebar-button/sidebar-button.module.scss' import styles from '../../sidebar-button/sidebar-button.module.scss'
import { ExportGistSidebarEntry } from './entries/export-gist-sidebar-entry/export-gist-sidebar-entry' import { ExportGistSidebarEntry } from './entries/export-gist-sidebar-entry/export-gist-sidebar-entry'
import { ExportGitlabSnippetSidebarEntry } from './entries/export-gitlab-snippet-sidebar-entry/export-gitlab-snippet-sidebar-entry' import { ExportGitlabSnippetSidebarEntry } from './entries/export-gitlab-snippet-sidebar-entry/export-gitlab-snippet-sidebar-entry'
import { ExportPrintSidebarEntry } from './entries/export-print-sidebar-entry'
/** /**
* Renders the export menu for the sidebar. * Renders the export menu for the sidebar.
@ -53,13 +54,15 @@ export const ExportSidebarMenu: React.FC<SpecificSidebarMenuProps> = ({
<Trans i18nKey={'editor.documentBar.export'} /> <Trans i18nKey={'editor.documentBar.export'} />
</SidebarButton> </SidebarButton>
<SidebarMenu expand={expand}> <SidebarMenu expand={expand}>
<ExportGistSidebarEntry /> <ExportPrintSidebarEntry />
<ExportGitlabSnippetSidebarEntry />
<ExportMarkdownSidebarEntry /> <ExportMarkdownSidebarEntry />
<SidebarButton icon={IconFileCode} disabled={true}> <SidebarButton icon={IconFileCode} disabled={true}>
HTML HTML
</SidebarButton> </SidebarButton>
<ExportGistSidebarEntry />
<ExportGitlabSnippetSidebarEntry />
<SidebarButton icon={IconFileCode} disabled={true}> <SidebarButton icon={IconFileCode} disabled={true}>
<Trans i18nKey='editor.export.rawHtml' /> <Trans i18nKey='editor.export.rawHtml' />
</SidebarButton> </SidebarButton>

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -35,7 +35,7 @@ export const NoteInfoLineWordCount: React.FC<NoteInfoLineWordCountProps> = ({ vi
const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady) const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady)
useEffect(() => { useEffect(() => {
if (rendererReady && visible) { if (rendererReady && visible) {
editorToRendererCommunicator.sendMessageToOtherSide({ type: CommunicationMessageType.GET_WORD_COUNT }) editorToRendererCommunicator?.sendMessageToOtherSide({ type: CommunicationMessageType.GET_WORD_COUNT })
} }
}, [editorToRendererCommunicator, rendererReady, visible]) }, [editorToRendererCommunicator, rendererReady, visible])

View file

@ -7,6 +7,7 @@ exports[`Splitter resize can change size with mouse 1`] = `
> >
<div <div
class="left" class="left"
id="editor-edit-pane"
style="width: calc(50% - 5px);" style="width: calc(50% - 5px);"
> >
<div <div
@ -18,6 +19,7 @@ exports[`Splitter resize can change size with mouse 1`] = `
<div <div
class="divider" class="divider"
data-testid="splitter-divider" data-testid="splitter-divider"
id="editor-splitter"
> >
<div <div
class="middle" class="middle"
@ -47,6 +49,7 @@ exports[`Splitter resize can change size with mouse 1`] = `
</div> </div>
<div <div
class="right" class="right"
id="editor-view-pane"
style="width: calc(100% - 50%);" style="width: calc(100% - 50%);"
> >
<div <div
@ -66,6 +69,7 @@ exports[`Splitter resize can change size with touch 1`] = `
> >
<div <div
class="left" class="left"
id="editor-edit-pane"
style="width: calc(50% - 5px);" style="width: calc(50% - 5px);"
> >
<div <div
@ -77,6 +81,7 @@ exports[`Splitter resize can change size with touch 1`] = `
<div <div
class="divider" class="divider"
data-testid="splitter-divider" data-testid="splitter-divider"
id="editor-splitter"
> >
<div <div
class="middle" class="middle"
@ -106,6 +111,7 @@ exports[`Splitter resize can change size with touch 1`] = `
</div> </div>
<div <div
class="right" class="right"
id="editor-view-pane"
style="width: calc(100% - 50%);" style="width: calc(100% - 50%);"
> >
<div <div
@ -125,6 +131,7 @@ exports[`Splitter resize can change size with touch 2`] = `
> >
<div <div
class="left" class="left"
id="editor-edit-pane"
style="width: calc(50% - 5px);" style="width: calc(50% - 5px);"
> >
<div <div
@ -136,6 +143,7 @@ exports[`Splitter resize can change size with touch 2`] = `
<div <div
class="divider" class="divider"
data-testid="splitter-divider" data-testid="splitter-divider"
id="editor-splitter"
> >
<div <div
class="middle" class="middle"
@ -165,6 +173,7 @@ exports[`Splitter resize can change size with touch 2`] = `
</div> </div>
<div <div
class="right" class="right"
id="editor-view-pane"
style="width: calc(100% - 50%);" style="width: calc(100% - 50%);"
> >
<div <div
@ -184,6 +193,7 @@ exports[`Splitter resize can change size with touch 3`] = `
> >
<div <div
class="left" class="left"
id="editor-edit-pane"
style="width: calc(50% - 5px);" style="width: calc(50% - 5px);"
> >
<div <div
@ -195,6 +205,7 @@ exports[`Splitter resize can change size with touch 3`] = `
<div <div
class="divider" class="divider"
data-testid="splitter-divider" data-testid="splitter-divider"
id="editor-splitter"
> >
<div <div
class="middle" class="middle"
@ -224,6 +235,7 @@ exports[`Splitter resize can change size with touch 3`] = `
</div> </div>
<div <div
class="right" class="right"
id="editor-view-pane"
style="width: calc(100% - 50%);" style="width: calc(100% - 50%);"
> >
<div <div
@ -243,6 +255,7 @@ exports[`Splitter resize can change size with touch 4`] = `
> >
<div <div
class="left" class="left"
id="editor-edit-pane"
style="width: calc(50% - 5px);" style="width: calc(50% - 5px);"
> >
<div <div
@ -254,6 +267,7 @@ exports[`Splitter resize can change size with touch 4`] = `
<div <div
class="divider" class="divider"
data-testid="splitter-divider" data-testid="splitter-divider"
id="editor-splitter"
> >
<div <div
class="middle" class="middle"
@ -283,6 +297,7 @@ exports[`Splitter resize can change size with touch 4`] = `
</div> </div>
<div <div
class="right" class="right"
id="editor-view-pane"
style="width: calc(100% - 50%);" style="width: calc(100% - 50%);"
> >
<div <div

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,58 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useIsRendererReady } from '../../render-page/window-post-message-communicator/hooks/use-is-renderer-ready'
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'
const TIMEOUT_BEFORE_PRINT = 25
/**
* Prints the content of the renderer iframe.
*/
export const usePrintIframe = (): (() => void) => {
const iframeCommunicator = useEditorToRendererCommunicator()
const rendererReady = useIsRendererReady()
return useCallback(() => {
if (!rendererReady) {
return
}
const iframe = document.getElementById('editor-renderer-iframe') as HTMLIFrameElement
if (!iframe || !iframe.contentWindow) {
return
}
iframeCommunicator?.sendMessageToOtherSide({
type: CommunicationMessageType.SET_PRINT_MODE,
printMode: true
})
setTimeout(() => {
iframe.contentWindow?.print()
iframeCommunicator?.sendMessageToOtherSide({
type: CommunicationMessageType.SET_PRINT_MODE,
printMode: false
})
}, TIMEOUT_BEFORE_PRINT)
}, [rendererReady, iframeCommunicator])
}
/**
* Print the content of the iframe from within the iframe.
*
* This should only be called if you're sure you are in the iframe e.g. `window.top === window.self`
*/
export const usePrintSelf = () => {
return useCallback(() => {
setPrintMode(true)
setTimeout(() => {
window.print()
setPrintMode(false)
}, TIMEOUT_BEFORE_PRINT)
}, [])
}

View file

@ -1,11 +1,16 @@
/* /*!
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
.click-shield { @media print {
.click-shield {
display: none !important;
}
}
.click-shield {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -10,9 +10,11 @@ import { ProxyImageFrame } from '../../extensions/image/proxy-image-frame'
import styles from './click-shield.module.scss' import styles from './click-shield.module.scss'
import type { Property } from 'csstype' import type { Property } from 'csstype'
import type { PropsWithChildren } from 'react' import type { PropsWithChildren } from 'react'
import { Fragment } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { Icon } from 'react-bootstrap-icons' import type { Icon } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { PrintLink } from './print-link'
const log = new Logger('OneClickEmbedding') const log = new Logger('OneClickEmbedding')
@ -23,6 +25,7 @@ export interface ClickShieldProps extends PropsWithChildren<PropsWithDataCypress
targetDescription: string targetDescription: string
containerClassName?: string containerClassName?: string
fallbackBackgroundColor?: Property.BackgroundColor fallbackBackgroundColor?: Property.BackgroundColor
fallbackLink: string
} }
/** /**
@ -44,6 +47,7 @@ export const ClickShield: React.FC<ClickShieldProps> = ({
targetDescription, targetDescription,
hoverIcon, hoverIcon,
fallbackBackgroundColor, fallbackBackgroundColor,
fallbackLink,
...props ...props
}) => { }) => {
const [showChildren, setShowChildren] = useState(false) const [showChildren, setShowChildren] = useState(false)
@ -114,23 +118,29 @@ export const ClickShield: React.FC<ClickShieldProps> = ({
if (showChildren) { if (showChildren) {
return ( return (
<span className={containerClassName} {...cypressId(props['data-cypress-id'])}> <Fragment>
{children} <span className={containerClassName} {...cypressId(props['data-cypress-id'])}>
</span> {children}
</span>
<PrintLink link={fallbackLink} />
</Fragment>
) )
} }
return ( return (
<span className={containerClassName} {...cypressId(props['data-cypress-id'])}> <Fragment>
<span className={`${styles['click-shield']} d-inline-block ratio ratio-16x9`} onClick={doShowChildren}> <span className={containerClassName} {...cypressId(props['data-cypress-id'])}>
{previewBackground} <span className={`d-inline-block ratio ratio-16x9 ${styles['click-shield']}`} onClick={doShowChildren}>
<span className={`${styles['preview-hover']}`}> {previewBackground}
<span> <span className={`${styles['preview-hover']}`}>
<Trans i18nKey={'renderer.clickShield.previewHoverText'} values={hoverTextTranslationValues} /> <span>
<Trans i18nKey={'renderer.clickShield.previewHoverText'} values={hoverTextTranslationValues} />
</span>
{icon}
</span> </span>
{icon}
</span> </span>
</span> </span>
</span> <PrintLink link={fallbackLink} />
</Fragment>
) )
} }

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
export interface PrintLinkProps {
link: string
}
/**
* Renders a link that is only visible in print-mode.
* This is required as a fallback for clickshield elements.
* @param link The link to render.
*/
export const PrintLink: React.FC<PrintLinkProps> = ({ link }) => {
return (
<p>
<a className={'print-only'} href={link}>
{link}
</a>
</p>
)
}

View file

@ -15,3 +15,9 @@
right: 0; right: 0;
} }
} }
@media print {
.markdown-toc-sidebar-button {
display: none;
}
}

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -17,6 +17,8 @@ import { countWords } from './word-counter'
import type { SlideOptions } from '@hedgedoc/commons' import type { SlideOptions } from '@hedgedoc/commons'
import { EventEmitter2 } from 'eventemitter2' 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 { usePrintKeyboardShortcut } from '../editor-page/hooks/use-print-keyboard-shortcut'
/** /**
* Wraps the markdown rendering in an iframe. * Wraps the markdown rendering in an iframe.
@ -78,6 +80,15 @@ export const RenderPageContent: React.FC = () => {
}, [communicator]) }, [communicator])
) )
useRendererReceiveHandler(
CommunicationMessageType.SET_PRINT_MODE,
useCallback(({ printMode }) => {
setPrintMode(printMode)
}, [])
)
usePrintKeyboardShortcut()
const onMakeScrollSource = useCallback(() => { const onMakeScrollSource = useCallback(() => {
sendScrolling.current = true sendScrolling.current = true
communicator.sendMessageToOtherSide({ communicator.sendMessageToOtherSide({

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;
}
}

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -23,7 +23,7 @@ export const useEditorReceiveHandler = <R extends RendererToEditorMessageType>(
if (!handler) { if (!handler) {
return return
} }
editorToRendererCommunicator.on(messageType, handler) editorToRendererCommunicator?.on(messageType, handler)
return () => editorToRendererCommunicator.off(messageType, handler) return () => editorToRendererCommunicator?.off(messageType, handler)
}, [editorToRendererCommunicator, handler, messageType]) }, [editorToRendererCommunicator, handler, messageType])
} }

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -22,7 +22,7 @@ export const useSendToRenderer = (
useEffect(() => { useEffect(() => {
if (message && rendererReady) { if (message && rendererReady) {
iframeCommunicator.sendMessageToOtherSide(message) iframeCommunicator?.sendMessageToOtherSide(message)
} }
}, [iframeCommunicator, message, rendererReady]) }, [iframeCommunicator, message, rendererReady])
} }

View file

@ -20,13 +20,19 @@ export enum CommunicationMessageType {
ON_WORD_COUNT_CALCULATED = 'ON_WORD_COUNT_CALCULATED', ON_WORD_COUNT_CALCULATED = 'ON_WORD_COUNT_CALCULATED',
SET_SLIDE_OPTIONS = 'SET_SLIDE_OPTIONS', SET_SLIDE_OPTIONS = 'SET_SLIDE_OPTIONS',
IMAGE_UPLOAD = 'IMAGE_UPLOAD', IMAGE_UPLOAD = 'IMAGE_UPLOAD',
EXTENSION_EVENT = 'EXTENSION_EVENT' EXTENSION_EVENT = 'EXTENSION_EVENT',
SET_PRINT_MODE = 'SET_PRINT_MODE'
} }
export interface NoPayloadMessage<TYPE extends CommunicationMessageType> { export interface NoPayloadMessage<TYPE extends CommunicationMessageType> {
type: TYPE type: TYPE
} }
export interface SetPrintModeConfigurationMessage {
type: CommunicationMessageType.SET_PRINT_MODE
printMode: boolean
}
export interface SetAdditionalConfigurationMessage { export interface SetAdditionalConfigurationMessage {
type: CommunicationMessageType.SET_ADDITIONAL_CONFIGURATION type: CommunicationMessageType.SET_ADDITIONAL_CONFIGURATION
darkModePreference: DarkModePreference darkModePreference: DarkModePreference
@ -101,6 +107,7 @@ export type CommunicationMessages =
| OnWordCountCalculatedMessage | OnWordCountCalculatedMessage
| ImageUploadMessage | ImageUploadMessage
| ExtensionEvent | ExtensionEvent
| SetPrintModeConfigurationMessage
export type EditorToRendererMessageType = export type EditorToRendererMessageType =
| CommunicationMessageType.SET_MARKDOWN_CONTENT | CommunicationMessageType.SET_MARKDOWN_CONTENT
@ -110,6 +117,7 @@ export type EditorToRendererMessageType =
| CommunicationMessageType.GET_WORD_COUNT | CommunicationMessageType.GET_WORD_COUNT
| CommunicationMessageType.SET_SLIDE_OPTIONS | CommunicationMessageType.SET_SLIDE_OPTIONS
| CommunicationMessageType.DISABLE_RENDERER_SCROLL_SOURCE | CommunicationMessageType.DISABLE_RENDERER_SCROLL_SOURCE
| CommunicationMessageType.SET_PRINT_MODE
export type RendererToEditorMessageType = export type RendererToEditorMessageType =
| CommunicationMessageType.RENDERER_READY | CommunicationMessageType.RENDERER_READY

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -30,8 +30,9 @@ export class IframeCapsuleReplacer extends ComponentReplacer {
<ClickShield <ClickShield
hoverIcon={IconGlobe} hoverIcon={IconGlobe}
targetDescription={node.attribs.src} targetDescription={node.attribs.src}
fallbackLink={node.attribs.src}
data-cypress-id={'iframe-capsule-click-shield'}> data-cypress-id={'iframe-capsule-click-shield'}>
{nativeRenderer()} <div className={'d-print-none'}>{nativeRenderer()}</div>
</ClickShield> </ClickShield>
) )
} }

View file

@ -1,10 +1,9 @@
/*! /*!
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
.abcjs-score { .abcjs-score {
:global(.markdown-body) & { :global(.markdown-body) & {
overflow-x: auto !important; overflow-x: auto !important;
@ -19,3 +18,16 @@
font-family: $font-family-base; font-family: $font-family-base;
} }
} }
@media print {
.abcjs-score {
:global(.markdown-body) & {
width: 100%;
height: auto;
overflow-x: hidden !important;
}
& > svg {
max-width: 100%;
}
}
}

View file

@ -5,7 +5,7 @@ exports[`Asciinema renders a click shield 1`] = `
<span> <span>
This is a click shield for This is a click shield for
<span <span
class="ratio ratio-16x9" class="ratio ratio-16x9 d-print-none"
> >
<iframe <iframe
allowfullscreen="" allowfullscreen=""

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -21,8 +21,9 @@ export const AsciinemaFrame: React.FC<IdProps> = ({ id }) => {
fallbackPreviewImageUrl={`https://asciinema.org/a/${id}.png`} fallbackPreviewImageUrl={`https://asciinema.org/a/${id}.png`}
fallbackBackgroundColor={'#d40000'} fallbackBackgroundColor={'#d40000'}
containerClassName={''} containerClassName={''}
fallbackLink={`https://asciinema.org/a/${id}`}
data-cypress-id={'click-shield-asciinema'}> data-cypress-id={'click-shield-asciinema'}>
<span className={'ratio ratio-16x9'}> <span className={'ratio ratio-16x9 d-print-none'}>
<iframe <iframe
allowFullScreen={true} allowFullScreen={true}
className='' className=''

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -31,9 +31,11 @@ export const GistFrame: React.FC<IdProps> = ({ id }) => {
fallbackBackgroundColor={'#161b22'} fallbackBackgroundColor={'#161b22'}
hoverIcon={IconGithub} hoverIcon={IconGithub}
targetDescription={'GitHub Gist'} targetDescription={'GitHub Gist'}
fallbackLink={`https://gist.github.com/${id}`}
data-cypress-id={'click-shield-gist'}> data-cypress-id={'click-shield-gist'}>
<iframe <iframe
sandbox='' sandbox=''
className={'d-print-none'}
{...cypressId('gh-gist')} {...cypressId('gh-gist')}
width='100%' width='100%'
height={`${frameHeight}px`} height={`${frameHeight}px`}
@ -41,7 +43,7 @@ export const GistFrame: React.FC<IdProps> = ({ id }) => {
title={`gist ${id}`} title={`gist ${id}`}
src={`https://gist.github.com/${id}.pibb`} src={`https://gist.github.com/${id}.pibb`}
/> />
<span className={styles['gist-resizer-row']}> <span className={`${styles['gist-resizer-row']} d-print-none`}>
<span className={styles['gist-resizer']} onMouseDown={onStart} onTouchStart={onStart} /> <span className={styles['gist-resizer']} onMouseDown={onStart} onTouchStart={onStart} />
</span> </span>
</ClickShield> </ClickShield>

View file

@ -5,7 +5,7 @@ exports[`VimeoFrame renders a click shield 1`] = `
<span> <span>
This is a click shield for This is a click shield for
<span <span
class="ratio ratio-16x9 d-inline-block" class="ratio ratio-16x9 d-inline-block d-print-none"
> >
<iframe <iframe
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -42,9 +42,10 @@ export const VimeoFrame: React.FC<IdProps> = ({ id }) => {
hoverIcon={IconVimeo} hoverIcon={IconVimeo}
targetDescription={'Vimeo'} targetDescription={'Vimeo'}
onImageFetch={getPreviewImageLink} onImageFetch={getPreviewImageLink}
fallbackLink={`https://vimeo.com/${id}`}
fallbackBackgroundColor={'#00adef'} fallbackBackgroundColor={'#00adef'}
data-cypress-id={'click-shield-vimeo'}> data-cypress-id={'click-shield-vimeo'}>
<span className={'ratio ratio-16x9 d-inline-block'}> <span className={'ratio ratio-16x9 d-inline-block d-print-none'}>
<iframe <iframe
title={`vimeo video of ${id}`} title={`vimeo video of ${id}`}
src={`https://player.vimeo.com/video/${id}?autoplay=1`} src={`https://player.vimeo.com/video/${id}?autoplay=1`}

View file

@ -5,7 +5,7 @@ exports[`YoutubeFrame renders a click shield 1`] = `
<span> <span>
This is a click shield for This is a click shield for
<span <span
class="ratio ratio-16x9 d-inline-block" class="ratio ratio-16x9 d-inline-block d-print-none"
> >
<iframe <iframe
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -19,9 +19,10 @@ export const YouTubeFrame: React.FC<IdProps> = ({ id }) => {
hoverIcon={IconYoutube} hoverIcon={IconYoutube}
targetDescription={'YouTube'} targetDescription={'YouTube'}
fallbackPreviewImageUrl={`https://i.ytimg.com/vi/${id}/maxresdefault.jpg`} fallbackPreviewImageUrl={`https://i.ytimg.com/vi/${id}/maxresdefault.jpg`}
fallbackLink={`https://www.youtube.com/watch?v=${id}`}
fallbackBackgroundColor={'#ff0000'} fallbackBackgroundColor={'#ff0000'}
data-cypress-id={'click-shield-youtube'}> data-cypress-id={'click-shield-youtube'}>
<span className={'ratio ratio-16x9 d-inline-block'}> <span className={'ratio ratio-16x9 d-inline-block d-print-none'}>
<iframe <iframe
title={`youtube video of ${id}`} title={`youtube video of ${id}`}
src={`https://www.youtube-nocookie.com/embed/${id}?autoplay=1`} src={`https://www.youtube-nocookie.com/embed/${id}?autoplay=1`}

View file

@ -1,11 +1,12 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { DarkModePreference } from '../../redux/dark-mode/types' import { DarkModePreference } from '../../redux/dark-mode/types'
import { useApplicationState } from '../common/use-application-state' import { useApplicationState } from '../common/use-application-state'
import useMediaQuery from '@restart/hooks/useMediaQuery' import useMediaQuery from '@restart/hooks/useMediaQuery'
import { useMemo } from 'react'
/** /**
* Uses the user settings and the browser preference to determine if dark mode should be used. * Uses the user settings and the browser preference to determine if dark mode should be used.
@ -14,7 +15,14 @@ import useMediaQuery from '@restart/hooks/useMediaQuery'
*/ */
export const useDarkModeState = (): boolean => { export const useDarkModeState = (): boolean => {
const preference = useApplicationState((state) => state.darkMode.darkModePreference) const preference = useApplicationState((state) => state.darkMode.darkModePreference)
const printModeEnabled = useApplicationState((state) => state.printMode)
const isBrowserPreferringDark = useMediaQuery('(prefers-color-scheme: dark)') const isBrowserPreferringDark = useMediaQuery('(prefers-color-scheme: dark)')
return preference === DarkModePreference.DARK || (preference === DarkModePreference.AUTO && isBrowserPreferringDark) return useMemo(() => {
if (printModeEnabled) {
return false
}
return preference === DarkModePreference.DARK || (preference === DarkModePreference.AUTO && isBrowserPreferringDark)
}, [preference, printModeEnabled, isBrowserPreferringDark])
} }

View file

@ -12,6 +12,7 @@ import { rendererStatusReducer } from './renderer-status/slice'
import { realtimeStatusReducer } from './realtime/slice' import { realtimeStatusReducer } from './realtime/slice'
import { historyReducer } from './history/slice' import { historyReducer } from './history/slice'
import { noteDetailsReducer } from './note-details/slice' import { noteDetailsReducer } from './note-details/slice'
import { printModeReducer } from './print-mode/slice'
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
@ -21,7 +22,8 @@ export const store = configureStore({
rendererStatus: rendererStatusReducer, rendererStatus: rendererStatusReducer,
realtimeStatus: realtimeStatusReducer, realtimeStatus: realtimeStatusReducer,
history: historyReducer, history: historyReducer,
noteDetails: noteDetailsReducer noteDetails: noteDetailsReducer,
printMode: printModeReducer
}, },
devTools: isDevMode devTools: isDevMode
}) })

View file

@ -0,0 +1,6 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const initialState: boolean = false

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { store } from '..'
import { printModeActionsCreator } from './slice'
export const setPrintMode = (printMode: boolean): void => {
const action = printModeActionsCreator.setPrintMode(printMode)
store.dispatch(action)
}

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
import { initialState } from './initial-state'
const printModeSlice = createSlice({
name: 'printMode',
initialState,
reducers: {
setPrintMode: (state, action: PayloadAction<boolean>) => {
state = action.payload
}
}
})
export const printModeActionsCreator = printModeSlice.actions
export const printModeReducer = printModeSlice.reducer

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -32,6 +32,7 @@ export const mockAppState = (state?: DeepPartial<ApplicationState>) => {
...initialStateDarkMode, ...initialStateDarkMode,
...state?.darkMode ...state?.darkMode
}, },
printMode: false,
editorConfig: { editorConfig: {
...initialStateEditorConfig, ...initialStateEditorConfig,
...state?.editorConfig ...state?.editorConfig