mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-13 06:34:39 -04: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> Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
parent
09e365ceea
commit
c5dd5eac0d
51 changed files with 520 additions and 80 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
73
frontend/global-styles/print.scss
Normal file
73
frontend/global-styles/print.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,3 +55,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.code-highlighter {
|
||||||
|
border: black solid 1px;
|
||||||
|
border-radius: var(--bs-border-radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
if (iframeCommunicator !== undefined) {
|
||||||
renderUrl.searchParams.set('uuid', iframeCommunicator.getUuid())
|
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)
|
||||||
|
|
|
@ -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'}
|
||||||
|
|
|
@ -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,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])
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
58
frontend/src/components/editor-page/utils/print-iframe.ts
Normal file
58
frontend/src/components/editor-page/utils/print-iframe.ts
Normal 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)
|
||||||
|
}, [])
|
||||||
|
}
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@media print {
|
||||||
.click-shield {
|
.click-shield {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-shield {
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -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,15 +118,19 @@ export const ClickShield: React.FC<ClickShieldProps> = ({
|
||||||
|
|
||||||
if (showChildren) {
|
if (showChildren) {
|
||||||
return (
|
return (
|
||||||
|
<Fragment>
|
||||||
<span className={containerClassName} {...cypressId(props['data-cypress-id'])}>
|
<span className={containerClassName} {...cypressId(props['data-cypress-id'])}>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
|
<PrintLink link={fallbackLink} />
|
||||||
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Fragment>
|
||||||
<span className={containerClassName} {...cypressId(props['data-cypress-id'])}>
|
<span className={containerClassName} {...cypressId(props['data-cypress-id'])}>
|
||||||
<span className={`${styles['click-shield']} d-inline-block ratio ratio-16x9`} onClick={doShowChildren}>
|
<span className={`d-inline-block ratio ratio-16x9 ${styles['click-shield']}`} onClick={doShowChildren}>
|
||||||
{previewBackground}
|
{previewBackground}
|
||||||
<span className={`${styles['preview-hover']}`}>
|
<span className={`${styles['preview-hover']}`}>
|
||||||
<span>
|
<span>
|
||||||
|
@ -132,5 +140,7 @@ export const ClickShield: React.FC<ClickShieldProps> = ({
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
<PrintLink link={fallbackLink} />
|
||||||
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -15,3 +15,9 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.markdown-toc-sidebar-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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=""
|
||||||
|
|
|
@ -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=''
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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`}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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`}
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
6
frontend/src/redux/print-mode/initial-state.ts
Normal file
6
frontend/src/redux/print-mode/initial-state.ts
Normal 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
|
12
frontend/src/redux/print-mode/methods.ts
Normal file
12
frontend/src/redux/print-mode/methods.ts
Normal 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)
|
||||||
|
}
|
21
frontend/src/redux/print-mode/slice.ts
Normal file
21
frontend/src/redux/print-mode/slice.ts
Normal 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
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue