Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e7edb9239 | |||
| 48cb2587be |
+5
-1
@@ -10,8 +10,12 @@ and this project adheres to
|
||||
## [Unreleased]
|
||||
|
||||
## Added
|
||||
|
||||
- 📝(doc) Add security.md and codeofconduct.md #604
|
||||
- ✨(frontend) add Alert, Quote, and Divider blocks to the editor #566
|
||||
|
||||
## Fixed
|
||||
|
||||
- 💄improve export spacings PDF #613
|
||||
|
||||
|
||||
## [2.1.0] - 2025-01-29
|
||||
|
||||
@@ -368,84 +368,4 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it checks the divider block', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'divider-block', browserName, 1);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
// Trigger slash menu to show menu
|
||||
await editor.click();
|
||||
await editor.fill('/');
|
||||
await page.getByText('Divider', { exact: true }).click();
|
||||
|
||||
await expect(
|
||||
editor.locator('.bn-block-content[data-content-type="divider"]'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('it checks the quote block', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'divider-block', browserName, 1);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
// Trigger slash menu to show menu
|
||||
await editor.click();
|
||||
await editor.fill('/');
|
||||
await page.getByText('Quote', { exact: true }).click();
|
||||
|
||||
await expect(
|
||||
editor.locator('.bn-block-content[data-content-type="quote"]'),
|
||||
).toBeVisible();
|
||||
|
||||
await editor.fill('Hello World');
|
||||
|
||||
await expect(editor.getByText('Hello World')).toHaveCSS(
|
||||
'font-style',
|
||||
'italic',
|
||||
);
|
||||
});
|
||||
|
||||
test('it checks the alert block', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'divider-block', browserName, 1);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
// Trigger slash menu to show menu
|
||||
await editor.click();
|
||||
await editor.fill('/');
|
||||
await page.getByText('Alert', { exact: true }).click();
|
||||
|
||||
const alertBlock = editor.locator(
|
||||
'.bn-block-content[data-content-type="alert"]',
|
||||
);
|
||||
await expect(
|
||||
alertBlock.locator('div[data-alert-type="warning"]'),
|
||||
).toBeVisible();
|
||||
await editor.fill('My alert');
|
||||
await expect(alertBlock.getByText('My alert')).toBeVisible();
|
||||
|
||||
await alertBlock.getByText('warning').click();
|
||||
|
||||
await expect(
|
||||
alertBlock.getByRole('menuitem', { name: 'warning Warning' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
alertBlock.getByRole('menuitem', { name: 'error Error' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
alertBlock.getByRole('menuitem', { name: 'info Info' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
alertBlock.getByRole('menuitem', { name: 'check_circle Success' }),
|
||||
).toBeVisible();
|
||||
|
||||
await alertBlock
|
||||
.getByRole('menuitem', { name: 'check_circle Success' })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
alertBlock.locator('div[data-alert-type="success"]'),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
+1
-28
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
BlockNoteEditor as BlockNoteEditorCore,
|
||||
BlockNoteSchema,
|
||||
Dictionary,
|
||||
defaultBlockSpecs,
|
||||
locales,
|
||||
} from '@blocknote/core';
|
||||
import { Dictionary, locales } from '@blocknote/core';
|
||||
import '@blocknote/core/fonts/inter.css';
|
||||
import { BlockNoteView } from '@blocknote/mantine';
|
||||
import '@blocknote/mantine/style.css';
|
||||
@@ -25,24 +19,7 @@ import { useEditorStore } from '../stores';
|
||||
import { cssEditor } from '../styles';
|
||||
import { randomColor } from '../utils';
|
||||
|
||||
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
|
||||
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||
import { AlertBlock, DividerBlock, QuoteBlock } from './custom-blocks';
|
||||
|
||||
export const schema = BlockNoteSchema.create({
|
||||
blockSpecs: {
|
||||
...defaultBlockSpecs,
|
||||
alert: AlertBlock,
|
||||
quote: QuoteBlock,
|
||||
divider: DividerBlock,
|
||||
},
|
||||
});
|
||||
|
||||
export type DocsBlockNoteEditor = BlockNoteEditorCore<
|
||||
typeof schema.blockSchema,
|
||||
typeof schema.inlineContentSchema,
|
||||
typeof schema.styleSchema
|
||||
>;
|
||||
|
||||
interface BlockNoteEditorProps {
|
||||
doc: Doc;
|
||||
@@ -101,7 +78,6 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
},
|
||||
},
|
||||
dictionary: locales[lang as keyof typeof locales] as Dictionary,
|
||||
schema,
|
||||
uploadFile,
|
||||
},
|
||||
[collabName, lang, provider, uploadFile],
|
||||
@@ -136,10 +112,8 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
editor={editor}
|
||||
formattingToolbar={false}
|
||||
editable={!readOnly}
|
||||
slashMenu={false}
|
||||
theme="light"
|
||||
>
|
||||
<BlockNoteSuggestionMenu />
|
||||
<BlockNoteToolbar />
|
||||
</BlockNoteView>
|
||||
</Box>
|
||||
@@ -165,7 +139,6 @@ export const BlockNoteEditorVersion = ({
|
||||
},
|
||||
provider: undefined,
|
||||
},
|
||||
schema,
|
||||
},
|
||||
[initialContent],
|
||||
);
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import { combineByGroup, filterSuggestionItems } from '@blocknote/core';
|
||||
import '@blocknote/mantine/style.css';
|
||||
import {
|
||||
SuggestionMenuController,
|
||||
getDefaultReactSlashMenuItems,
|
||||
useBlockNoteEditor,
|
||||
} from '@blocknote/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DocsBlockNoteEditor } from './BlockNoteEditor';
|
||||
import { insertAlert, insertDivider, insertQuote } from './custom-blocks';
|
||||
|
||||
export const BlockNoteSuggestionMenu = () => {
|
||||
const editor = useBlockNoteEditor() as DocsBlockNoteEditor;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getSlashMenuItems = useMemo(() => {
|
||||
return async (query: string) =>
|
||||
Promise.resolve(
|
||||
filterSuggestionItems(
|
||||
combineByGroup(
|
||||
getDefaultReactSlashMenuItems(editor),
|
||||
[insertAlert(editor, t)],
|
||||
[insertQuote(editor, t)],
|
||||
[insertDivider(editor, t)],
|
||||
),
|
||||
query,
|
||||
),
|
||||
);
|
||||
}, [editor, t]);
|
||||
|
||||
return (
|
||||
<SuggestionMenuController
|
||||
triggerCharacter="/"
|
||||
getItems={getSlashMenuItems}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { defaultProps, insertOrUpdateBlock } from '@blocknote/core';
|
||||
import { createReactBlockSpec } from '@blocknote/react';
|
||||
import { Menu } from '@mantine/core';
|
||||
import { TFunction } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../BlockNoteEditor';
|
||||
|
||||
// The types of alerts that users can choose from.
|
||||
export const alertTypes = [
|
||||
{
|
||||
title: 'Warning',
|
||||
value: 'warning',
|
||||
icon: 'warning',
|
||||
color: 'warning-500',
|
||||
backgroundColor: 'warning-300',
|
||||
},
|
||||
{
|
||||
title: 'Error',
|
||||
value: 'danger',
|
||||
icon: 'error',
|
||||
color: 'danger-500',
|
||||
backgroundColor: 'danger-300',
|
||||
},
|
||||
{
|
||||
title: 'Info',
|
||||
value: 'info',
|
||||
icon: 'info',
|
||||
color: 'info-500',
|
||||
backgroundColor: 'info-300',
|
||||
},
|
||||
{
|
||||
title: 'Success',
|
||||
value: 'success',
|
||||
icon: 'check_circle',
|
||||
color: 'success-500',
|
||||
backgroundColor: 'success-100',
|
||||
},
|
||||
] as const;
|
||||
|
||||
// The Alert block.
|
||||
export const AlertBlock = createReactBlockSpec(
|
||||
{
|
||||
type: 'alert',
|
||||
propSchema: {
|
||||
textAlignment: defaultProps.textAlignment,
|
||||
textColor: defaultProps.textColor,
|
||||
type: {
|
||||
default: 'warning',
|
||||
values: ['warning', 'danger', 'info', 'success'],
|
||||
},
|
||||
},
|
||||
content: 'inline',
|
||||
},
|
||||
{
|
||||
render: (props) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { t } = useTranslation();
|
||||
let alertType = alertTypes.find(
|
||||
(a) => a.value === props.block.props.type,
|
||||
);
|
||||
|
||||
if (!alertType) {
|
||||
alertType = alertTypes[0];
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="alert"
|
||||
data-alert-type={props.block.props.type}
|
||||
$direction="row"
|
||||
$justify="center"
|
||||
$align="center"
|
||||
$radius="4px"
|
||||
$padding="4px"
|
||||
$background={colorsTokens()[alertType.backgroundColor]}
|
||||
$minHeight="48px"
|
||||
$css={css`
|
||||
flex-grow: 1;
|
||||
`}
|
||||
>
|
||||
<Menu withinPortal={false}>
|
||||
<Menu.Target>
|
||||
<Box
|
||||
className="alert-icon-wrapper"
|
||||
$margin={{ horizontal: '12px' }}
|
||||
$radius="16px"
|
||||
$justify="center"
|
||||
$align="center"
|
||||
$height="24px"
|
||||
$width="24px"
|
||||
contentEditable={false}
|
||||
$css="user-select: none; cursor: pointer;"
|
||||
>
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$theme={alertType.value}
|
||||
$variation="500"
|
||||
$size="20px"
|
||||
>
|
||||
{alertType.icon}
|
||||
</Text>
|
||||
</Box>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown style={{ zIndex: 9999 }}>
|
||||
<Menu.Label>{t('Alert Type')}</Menu.Label>
|
||||
<Menu.Divider />
|
||||
{alertTypes.map((type) => (
|
||||
<Menu.Item
|
||||
key={type.value}
|
||||
leftSection={
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$color={colorsTokens()[type.color]}
|
||||
$size="16px"
|
||||
>
|
||||
{type.icon}
|
||||
</Text>
|
||||
}
|
||||
onClick={() =>
|
||||
props.editor.updateBlock(props.block, {
|
||||
type: 'alert',
|
||||
props: { type: type.value },
|
||||
})
|
||||
}
|
||||
>
|
||||
{t(type.title)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
<Box
|
||||
className="inline-content"
|
||||
$css={css`
|
||||
flex-grow: 1;
|
||||
& * {
|
||||
color: ${colorsTokens()[alertType.color]};
|
||||
}
|
||||
`}
|
||||
ref={props.contentRef}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const insertAlert = (
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
) => ({
|
||||
title: t('Alert'),
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlock(editor, {
|
||||
type: 'alert',
|
||||
});
|
||||
},
|
||||
aliases: [
|
||||
'alert',
|
||||
'notification',
|
||||
'emphasize',
|
||||
'warning',
|
||||
'error',
|
||||
'info',
|
||||
'success',
|
||||
],
|
||||
group: t('Others'),
|
||||
icon: (
|
||||
<Text $isMaterialIcon $size="18px">
|
||||
warning
|
||||
</Text>
|
||||
),
|
||||
subtext: t('Add a colored alert box'),
|
||||
});
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import { defaultProps, insertOrUpdateBlock } from '@blocknote/core';
|
||||
import { createReactBlockSpec } from '@blocknote/react';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../BlockNoteEditor';
|
||||
|
||||
export const DividerBlock = createReactBlockSpec(
|
||||
{
|
||||
type: 'divider',
|
||||
propSchema: {
|
||||
textAlignment: defaultProps.textAlignment,
|
||||
textColor: defaultProps.textColor,
|
||||
},
|
||||
content: 'none',
|
||||
},
|
||||
{
|
||||
render: () => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '2px',
|
||||
backgroundColor: colorsTokens()['greyscale-300'],
|
||||
margin: '1rem 0',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const insertDivider = (
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
) => ({
|
||||
title: t('Divider'),
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlock(editor, {
|
||||
type: 'divider',
|
||||
});
|
||||
},
|
||||
aliases: ['divider', 'hr', 'horizontal rule', 'line', 'separator'],
|
||||
group: t('Others'),
|
||||
icon: (
|
||||
<span className="material-icons" style={{ fontSize: '18px' }}>
|
||||
remove
|
||||
</span>
|
||||
),
|
||||
subtext: t('Add a horizontal line'),
|
||||
});
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
import { defaultProps, insertOrUpdateBlock } from '@blocknote/core';
|
||||
import { createReactBlockSpec } from '@blocknote/react';
|
||||
import { TFunction } from 'i18next';
|
||||
import React from 'react';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../BlockNoteEditor';
|
||||
|
||||
export const QuoteBlock = createReactBlockSpec(
|
||||
{
|
||||
type: 'quote',
|
||||
propSchema: {
|
||||
textAlignment: defaultProps.textAlignment,
|
||||
textColor: defaultProps.textColor,
|
||||
},
|
||||
content: 'inline',
|
||||
},
|
||||
{
|
||||
render: (props) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="inline-content"
|
||||
style={{
|
||||
borderLeft: `4px solid ${colorsTokens()['greyscale-300']}`,
|
||||
margin: '0 0 1rem 0',
|
||||
padding: '0.5rem 1rem',
|
||||
color: colorsTokens()['greyscale-600'],
|
||||
fontStyle: 'italic',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
ref={props.contentRef}
|
||||
/>
|
||||
);
|
||||
},
|
||||
parse: () => {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const insertQuote = (
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
) => ({
|
||||
title: t('Quote'),
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlock(editor, {
|
||||
type: 'quote',
|
||||
});
|
||||
},
|
||||
aliases: ['quote', 'blockquote', 'citation'],
|
||||
group: t('Others'),
|
||||
icon: (
|
||||
<span className="material-icons" style={{ fontSize: '18px' }}>
|
||||
format_quote
|
||||
</span>
|
||||
),
|
||||
subtext: t('Add a quote block'),
|
||||
});
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
export * from './AlertBlock';
|
||||
export * from './DividerBlock';
|
||||
export * from './QuoteBlock';
|
||||
@@ -1,9 +1,9 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../components/BlockNoteEditor';
|
||||
import { useHeadingStore } from '../stores';
|
||||
|
||||
export const useHeadings = (editor: DocsBlockNoteEditor) => {
|
||||
export const useHeadings = (editor: BlockNoteEditor) => {
|
||||
const { setHeadings, resetHeadings } = useHeadingStore();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../components/BlockNoteEditor';
|
||||
|
||||
export interface UseEditorstore {
|
||||
editor?: DocsBlockNoteEditor;
|
||||
setEditor: (editor: DocsBlockNoteEditor | undefined) => void;
|
||||
editor?: BlockNoteEditor;
|
||||
setEditor: (editor: BlockNoteEditor | undefined) => void;
|
||||
}
|
||||
|
||||
export const useEditorStore = create<UseEditorstore>((set) => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../components/BlockNoteEditor';
|
||||
import { HeadingBlock } from '../types';
|
||||
|
||||
const recursiveTextContent = (content: HeadingBlock['content']): string => {
|
||||
@@ -21,7 +21,7 @@ const recursiveTextContent = (content: HeadingBlock['content']): string => {
|
||||
|
||||
export interface UseHeadingStore {
|
||||
headings: HeadingBlock[];
|
||||
setHeadings: (editor: DocsBlockNoteEditor) => void;
|
||||
setHeadings: (editor: BlockNoteEditor) => void;
|
||||
resetHeadings: () => void;
|
||||
}
|
||||
|
||||
|
||||
-9
@@ -6,15 +6,6 @@ export const cssEditor = (readonly: boolean) => css`
|
||||
& .ProseMirror {
|
||||
height: 100%;
|
||||
|
||||
.bn-side-menu[data-block-type='alert'] {
|
||||
height: 55px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='divider'] {
|
||||
height: 40px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='quote'] {
|
||||
height: 46px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='1'] {
|
||||
height: 50px;
|
||||
}
|
||||
@@ -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
|
||||
>;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<typeof Font.getEmojiSource>;
|
||||
};
|
||||
|
||||
type DocsDefaultProps = DefaultProps & {
|
||||
level?: number;
|
||||
};
|
||||
|
||||
export class DocsPDFExporter extends PDFExporter<
|
||||
DocsBlockSchema,
|
||||
DocsStyleSchema,
|
||||
DocsInlineContentSchema
|
||||
> {
|
||||
constructor(
|
||||
protected readonly schemaMappings: DocsBlockNoteSchema,
|
||||
options?: Partial<Options>,
|
||||
) {
|
||||
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<DocsBlockSchema, DocsInlineContentSchema, DocsStyleSchema>[], // Or BlockFromConfig<B[keyof B], I, S>?
|
||||
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<DocsDefaultProps>,
|
||||
) {
|
||||
let styles = super.blocknoteDefaultPropsToReactPDFStyle(props);
|
||||
|
||||
// Add margin to headings
|
||||
if (props.level) {
|
||||
styles = {
|
||||
marginTop: 15,
|
||||
marginBottom: 15,
|
||||
...styles,
|
||||
};
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -1,11 +1,10 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { BoxButton, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../../doc-editor/components/BlockNoteEditor';
|
||||
|
||||
const leftPaddingMap: { [key: number]: string } = {
|
||||
3: '1.5rem',
|
||||
2: '0.9rem',
|
||||
@@ -18,7 +17,7 @@ export type HeadingsHighlight = {
|
||||
}[];
|
||||
|
||||
interface HeadingProps {
|
||||
editor: DocsBlockNoteEditor;
|
||||
editor: BlockNoteEditor;
|
||||
level: number;
|
||||
text: string;
|
||||
headingId: string;
|
||||
|
||||
Reference in New Issue
Block a user