From 48cb2587be8a3594d842ac89c285802ade33a355 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 5 Feb 2025 15:23:25 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=92=84(frontend)=20improve=20styles?= =?UTF-8?q?=20export=20pdf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When exporting a document to PDF, the headings spacings were too small, the break lines were not displayed. This commit fixes these issues by extending PDFExporter. --- CHANGELOG.md | 6 ++ .../src/features/docs/doc-editor/types.tsx | 18 ++++ .../doc-header/components/ModalExport.tsx | 22 ++--- .../docs/doc-header/libs/DocsPDFExporter.ts | 87 +++++++++++++++++++ 4 files changed, 117 insertions(+), 16 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-header/libs/DocsPDFExporter.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a29f4cf817..3f555bc861 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,14 @@ and this project adheres to ## [Unreleased] ## Added + - 📝(doc) Add security.md and codeofconduct.md #604 +## Fixed + +- 💄improve export spacings PDF #613 + + ## [2.1.0] - 2025-01-29 ## Added diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/types.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/types.tsx index 19094b6c54..36dbe0ce18 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/types.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/types.tsx @@ -1,3 +1,5 @@ +import { BlockNoteEditor, BlockNoteSchema } from '@blocknote/core'; + export interface DocAttachment { file: string; } @@ -12,3 +14,19 @@ export type HeadingBlock = { level: number; }; }; + +export const blockNoteInstance = BlockNoteSchema.create(); +export type DocsBlockSchema = typeof blockNoteInstance.blockSchema; +export type DocsInlineContentSchema = + typeof blockNoteInstance.inlineContentSchema; +export type DocsStyleSchema = typeof blockNoteInstance.styleSchema; +export type DocsBlockNoteEditor = BlockNoteEditor< + DocsBlockSchema, + DocsInlineContentSchema, + DocsStyleSchema +>; +export type DocsBlockNoteSchema = BlockNoteSchema< + DocsBlockSchema, + DocsInlineContentSchema, + DocsStyleSchema +>; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/ModalExport.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/ModalExport.tsx index 7d7f1657fd..fddd7351fa 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/ModalExport.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/ModalExport.tsx @@ -2,10 +2,6 @@ import { DOCXExporter, docxDefaultSchemaMappings, } from '@blocknote/xl-docx-exporter'; -import { - PDFExporter, - pdfDefaultSchemaMappings, -} from '@blocknote/xl-pdf-exporter'; import { Button, Loader, @@ -25,6 +21,7 @@ import { useEditorStore } from '@/features/docs/doc-editor'; import { Doc } from '@/features/docs/doc-management'; import { TemplatesOrdering, useTemplates } from '../api/useTemplates'; +import { DocsPDFExporter } from '../libs/DocsPDFExporter'; import { downloadFile, exportResolveFileUrl } from '../utils'; enum DocDownloadFormat { @@ -91,19 +88,12 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => { let blobExport: Blob; if (format === DocDownloadFormat.PDF) { - const defaultExporter = new PDFExporter( - editor.schema, - pdfDefaultSchemaMappings, - ); + const defaultExporter = new DocsPDFExporter(editor.schema); + const exporter = new DocsPDFExporter(editor.schema, { + resolveFileUrl: async (url) => + exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl), + }); - const exporter = new PDFExporter( - editor.schema, - pdfDefaultSchemaMappings, - { - resolveFileUrl: async (url) => - exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl), - }, - ); const pdfDocument = await exporter.toReactPDFDocument(exportDocument); blobExport = await pdf(pdfDocument).toBlob(); } else { diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/libs/DocsPDFExporter.ts b/src/frontend/apps/impress/src/features/docs/doc-header/libs/DocsPDFExporter.ts new file mode 100644 index 0000000000..a2be7ea986 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-header/libs/DocsPDFExporter.ts @@ -0,0 +1,87 @@ +import { Block, DefaultProps, ExporterOptions } from '@blocknote/core'; +import { + PDFExporter, + pdfDefaultSchemaMappings, +} from '@blocknote/xl-pdf-exporter'; +import { Font } from '@react-pdf/renderer'; + +import { + DocsBlockNoteSchema, + DocsBlockSchema, + DocsInlineContentSchema, + DocsStyleSchema, +} from '@/features/docs/doc-editor'; + +type Options = ExporterOptions & { + emojiSource: false | ReturnType; +}; + +type DocsDefaultProps = DefaultProps & { + level?: number; +}; + +export class DocsPDFExporter extends PDFExporter< + DocsBlockSchema, + DocsStyleSchema, + DocsInlineContentSchema +> { + constructor( + protected readonly schemaMappings: DocsBlockNoteSchema, + options?: Partial, + ) { + super(schemaMappings, pdfDefaultSchemaMappings, options); + } + + /** + * Breaklines are not displayed in PDFs, by adding a space we ensure that the line is not ignored + * @param blocks + * @param nestingLevel + * @returns + */ + public transformBlocks( + blocks: Block[], // Or BlockFromConfig? + nestingLevel?: number, + ) { + blocks.forEach((block) => { + if (Array.isArray(block.content)) { + block.content.forEach((content) => { + if (content.type === 'text' && !content.text) { + content.text = ' '; + } + }); + + if (!block.content.length) { + block.content.push({ + styles: {}, + text: ' ', + type: 'text', + }); + } + } + }); + + return super.transformBlocks(blocks, nestingLevel); + } + + /** + * Override the method to add our custom styles + * @param props + * @returns + */ + public blocknoteDefaultPropsToReactPDFStyle( + props: Partial, + ) { + let styles = super.blocknoteDefaultPropsToReactPDFStyle(props); + + // Add margin to headings + if (props.level) { + styles = { + marginTop: 15, + marginBottom: 15, + ...styles, + }; + } + + return styles; + } +} From 0e7edb9239031eb9bd57ee32f79b0dfbeef60edb Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 5 Feb 2025 15:28:49 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9A=9A(frontend)=20move=20Blocknote?= =?UTF-8?q?=20styles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move Blocknote styles to separate file. --- .../doc-editor/components/BlockNoteEditor.tsx | 88 +------------------ .../src/features/docs/doc-editor/styles.tsx | 87 ++++++++++++++++++ 2 files changed, 88 insertions(+), 87 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 326163ddbe..fb4f79648e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -6,7 +6,6 @@ import { useCreateBlockNote } from '@blocknote/react'; import { HocuspocusProvider } from '@hocuspocus/provider'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { css } from 'styled-components'; import * as Y from 'yjs'; import { Box, TextErrors } from '@/components'; @@ -17,96 +16,11 @@ import { useUploadFile } from '../hook'; import { useHeadings } from '../hook/useHeadings'; import useSaveDoc from '../hook/useSaveDoc'; import { useEditorStore } from '../stores'; +import { cssEditor } from '../styles'; import { randomColor } from '../utils'; import { BlockNoteToolbar } from './BlockNoteToolbar'; -const cssEditor = (readonly: boolean) => css` - &, - & > .bn-container, - & .ProseMirror { - height: 100%; - - .bn-side-menu[data-block-type='heading'][data-level='1'] { - height: 50px; - } - .bn-side-menu[data-block-type='heading'][data-level='2'] { - height: 43px; - } - .bn-side-menu[data-block-type='heading'][data-level='3'] { - height: 35px; - } - h1 { - font-size: 1.875rem; - } - h2 { - font-size: 1.5rem; - } - h3 { - font-size: 1.25rem; - } - a { - color: var(--c--theme--colors--greyscale-500); - cursor: pointer; - } - .bn-block-group - .bn-block-group - .bn-block-outer:not([data-prev-depth-changed]):before { - border-left: none; - } - } - - .bn-editor { - color: var(--c--theme--colors--greyscale-700); - } - - .bn-block-outer:not(:first-child) { - &:has(h1) { - padding-top: 32px; - } - &:has(h2) { - padding-top: 24px; - } - &:has(h3) { - padding-top: 16px; - } - } - - & .bn-inline-content code { - background-color: gainsboro; - padding: 2px; - border-radius: 4px; - } - - @media screen and (width <= 560px) { - & .bn-editor { - ${readonly && `padding-left: 10px;`} - } - .bn-side-menu[data-block-type='heading'][data-level='1'] { - height: 46px; - } - .bn-side-menu[data-block-type='heading'][data-level='2'] { - height: 40px; - } - .bn-side-menu[data-block-type='heading'][data-level='3'] { - height: 40px; - } - & .bn-editor h1 { - font-size: 1.6rem; - } - & .bn-editor h2 { - font-size: 1.35rem; - } - & .bn-editor h3 { - font-size: 1.2rem; - } - .bn-block-content[data-is-empty-and-focused][data-content-type='paragraph'] - .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before { - font-size: 14px; - } - } -`; - interface BlockNoteEditorProps { doc: Doc; provider: HocuspocusProvider; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx new file mode 100644 index 0000000000..964d243343 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx @@ -0,0 +1,87 @@ +import { css } from 'styled-components'; + +export const cssEditor = (readonly: boolean) => css` + &, + & > .bn-container, + & .ProseMirror { + height: 100%; + + .bn-side-menu[data-block-type='heading'][data-level='1'] { + height: 50px; + } + .bn-side-menu[data-block-type='heading'][data-level='2'] { + height: 43px; + } + .bn-side-menu[data-block-type='heading'][data-level='3'] { + height: 35px; + } + h1 { + font-size: 1.875rem; + } + h2 { + font-size: 1.5rem; + } + h3 { + font-size: 1.25rem; + } + a { + color: var(--c--theme--colors--greyscale-500); + cursor: pointer; + } + .bn-block-group + .bn-block-group + .bn-block-outer:not([data-prev-depth-changed]):before { + border-left: none; + } + } + + .bn-editor { + color: var(--c--theme--colors--greyscale-700); + } + + .bn-block-outer:not(:first-child) { + &:has(h1) { + padding-top: 32px; + } + &:has(h2) { + padding-top: 24px; + } + &:has(h3) { + padding-top: 16px; + } + } + + & .bn-inline-content code { + background-color: gainsboro; + padding: 2px; + border-radius: 4px; + } + + @media screen and (width <= 560px) { + & .bn-editor { + ${readonly && `padding-left: 10px;`} + } + .bn-side-menu[data-block-type='heading'][data-level='1'] { + height: 46px; + } + .bn-side-menu[data-block-type='heading'][data-level='2'] { + height: 40px; + } + .bn-side-menu[data-block-type='heading'][data-level='3'] { + height: 40px; + } + & .bn-editor h1 { + font-size: 1.6rem; + } + & .bn-editor h2 { + font-size: 1.35rem; + } + & .bn-editor h3 { + font-size: 1.2rem; + } + .bn-block-content[data-is-empty-and-focused][data-content-type='paragraph'] + .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before { + font-size: 14px; + } + } +`;