mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-05 17:27:48 +02:00
✨(frontend) add math and diagram blocks to the editor
Last version of Blocknote added support for math and diagram blocks. This commit updates the dependencies to include the necessary toolbar items to create math and diagram blocks in the editor.
This commit is contained in:
@@ -11,6 +11,7 @@ and this project adheres to
|
||||
- ✨(frontend) Add "Copy link to block" feature #2547
|
||||
- ✨(frontend) add word count to doc header toolbox #2549
|
||||
- ✨(frontend) add find and replace feature to the editor #2570
|
||||
- ✨(frontend) add math and diagram blocks to the editor #2617
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
Binary file not shown.
@@ -28,7 +28,7 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor, nisl ege
|
||||
|
||||
**Code block:**
|
||||
|
||||
```js
|
||||
```javascript
|
||||
const hello_world = () => {
|
||||
console.log("Hello, world!");
|
||||
}
|
||||
|
||||
@@ -709,7 +709,7 @@ test.describe('Doc Editor', () => {
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await createDoc(page, 'doc-scroll', browserName, 1);
|
||||
await createDoc(page, 'doc-copy-link-to-block', browserName, 1);
|
||||
|
||||
const editor = await writeInEditor({ page, text: 'First Block' });
|
||||
|
||||
@@ -745,4 +745,45 @@ test.describe('Doc Editor', () => {
|
||||
await expect(editor.getByText('First Block')).not.toBeInViewport();
|
||||
await expect(editor.getByText('My Block')).toBeInViewport();
|
||||
});
|
||||
|
||||
test('it checks "Equation block" feature', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'doc-equation', browserName, 1);
|
||||
|
||||
const { editor } = await openSuggestionMenu({
|
||||
page,
|
||||
suggestion: 'Block Equation',
|
||||
});
|
||||
|
||||
await editor.getByLabel('E = mc^2').fill('E = mc^2');
|
||||
await editor.locator('.bn-code-block-source-popup-ok-button').click();
|
||||
await expect(
|
||||
editor.locator('.katex-html').filter({ hasText: 'E=mc2' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('it checks "Diagram block" feature', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'doc-diagram', browserName, 1);
|
||||
|
||||
const { editor } = await openSuggestionMenu({
|
||||
page,
|
||||
suggestion: 'Diagram',
|
||||
});
|
||||
|
||||
await editor.getByRole('img', { name: 'Mermaid diagram' }).click();
|
||||
|
||||
const diagramCode = editor.getByLabel('Enter diagram code');
|
||||
await diagramCode.click();
|
||||
await page.keyboard.press('ControlOrMeta+a');
|
||||
await page.keyboard.press('Backspace');
|
||||
|
||||
await page.keyboard.type('graph TD');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.type(' A[Hello] --> B[World]');
|
||||
|
||||
await editor.locator('.bn-code-block-source-popup-ok-button').click();
|
||||
|
||||
await expect(
|
||||
editor.getByLabel('Mermaid diagram').filter({ hasText: 'HelloWorld' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -360,7 +360,7 @@ test.describe('Doc Export', () => {
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
|
||||
|
||||
@@ -357,6 +357,9 @@ test.describe('Doc Tree', () => {
|
||||
const treeRow1 = await getTreeRow(page, docChild1);
|
||||
const treeRow2 = await getTreeRow(page, docChild2);
|
||||
|
||||
// Wait for the tree to be stable
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await docTree.click();
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(rootItem).toBeFocused();
|
||||
|
||||
@@ -11,9 +11,14 @@ export const getEditor = async ({ page }: { page: Page }) => {
|
||||
export const tryFocusEditorContent = async ({ page }: { page: Page }) => {
|
||||
const editor = await getEditor({ page });
|
||||
if (
|
||||
(await editor.locator('.bn-trailing-block.ProseMirror-widget').count()) > 0
|
||||
(await editor
|
||||
.locator('.bn-block-outer div[data-content-type="paragraph"]')
|
||||
.count()) > 0
|
||||
) {
|
||||
await editor.locator('.bn-trailing-block.ProseMirror-widget').click();
|
||||
await editor
|
||||
.locator('.bn-block-outer div[data-content-type="paragraph"]')
|
||||
.last()
|
||||
.click();
|
||||
} else {
|
||||
await editor.click();
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export const overrideDocContent = async ({
|
||||
1,
|
||||
);
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(page.getByText('copy/pasting out of doc')).toBeVisible();
|
||||
|
||||
// Add Image SVG
|
||||
await openSuggestionMenu({
|
||||
@@ -178,7 +178,7 @@ export const comparePDFWithAssetFolder = async ({
|
||||
const maxDiffRatio = 0.0005;
|
||||
|
||||
try {
|
||||
expect(numDiffPixels).toBeLessThan(0.0005);
|
||||
expect(diffRatio).toBeLessThan(maxDiffRatio);
|
||||
} catch {
|
||||
if (testInfo) {
|
||||
const pageNo = String(i + 1).padStart(2, '0');
|
||||
|
||||
@@ -11,6 +11,10 @@ const nextConfig = {
|
||||
allowedDevOrigins: ['docs.127.0.0.1.nip.io'],
|
||||
output: 'export',
|
||||
trailingSlash: true,
|
||||
// `@blocknote/math-block` imports `katex/dist/katex.min.css` from its entry
|
||||
// point; transpiling it lets Next.js accept that global CSS import from
|
||||
// within node_modules.
|
||||
transpilePackages: ['@blocknote/math-block'],
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
"@ai-sdk/openai": "3.0.71",
|
||||
"@blocknote/code-block": "0.54.0",
|
||||
"@blocknote/core": "0.54.0",
|
||||
"@blocknote/diagram-block": "0.54.0",
|
||||
"@blocknote/mantine": "0.54.0",
|
||||
"@blocknote/math-block": "0.54.0",
|
||||
"@blocknote/react": "0.54.0",
|
||||
"@blocknote/xl-ai": "0.54.0",
|
||||
"@blocknote/xl-docx-exporter": "0.54.0",
|
||||
@@ -46,6 +48,7 @@
|
||||
"@mantine/core": "9.5.2",
|
||||
"@mantine/hooks": "9.5.2",
|
||||
"@react-aria/live-announcer": "3.5.1",
|
||||
"@react-pdf/math": "2.0.1",
|
||||
"@react-pdf/renderer": "4.9.0",
|
||||
"@sentry/nextjs": "10.70.0",
|
||||
"@tanstack/react-query": "5.102.2",
|
||||
@@ -63,6 +66,8 @@
|
||||
"idb": "8.0.3",
|
||||
"lodash": "4.18.1",
|
||||
"luxon": "3.7.2",
|
||||
"mathjax-full": "3.2.2",
|
||||
"mathml2omml": "0.5.0",
|
||||
"next": "16.3.2",
|
||||
"posthog-js": "1.418.10",
|
||||
"react": "*",
|
||||
|
||||
+27
-1
@@ -1,4 +1,4 @@
|
||||
import { codeBlockOptions } from '@blocknote/code-block';
|
||||
import { codeBlockOptions, syntaxHighlighter } from '@blocknote/code-block';
|
||||
import {
|
||||
BlockNoteSchema,
|
||||
createCodeBlockSpec,
|
||||
@@ -10,8 +10,17 @@ import { CommentsExtension } from '@blocknote/core/comments';
|
||||
import '@blocknote/core/fonts/inter.css';
|
||||
import * as localesBN from '@blocknote/core/locales';
|
||||
import { withCollaboration } from '@blocknote/core/yjs';
|
||||
import {
|
||||
createReactDiagramBlockSpec,
|
||||
locales as diagramLocales,
|
||||
} from '@blocknote/diagram-block';
|
||||
import { BlockNoteView } from '@blocknote/mantine';
|
||||
import '@blocknote/mantine/style.css';
|
||||
import {
|
||||
createReactInlineMathSpec,
|
||||
createReactMathBlockSpec,
|
||||
locales as mathLocales,
|
||||
} from '@blocknote/math-block';
|
||||
import {
|
||||
FloatingComposerController,
|
||||
FloatingThreadController,
|
||||
@@ -75,12 +84,15 @@ const baseBlockNoteSchema = withPageBreak(
|
||||
...defaultBlockSpecs,
|
||||
callout: CalloutBlock(),
|
||||
codeBlock: createCodeBlockSpec(codeBlockOptions),
|
||||
diagram: createReactDiagramBlockSpec(),
|
||||
mathBlock: createReactMathBlockSpec(),
|
||||
pdf: PdfBlock(),
|
||||
uploadLoader: UploadLoaderBlock(),
|
||||
},
|
||||
inlineContentSpecs: {
|
||||
...defaultInlineContentSpecs,
|
||||
interlinkingLinkInline: InterlinkingLinkInlineContent,
|
||||
math: createReactInlineMathSpec(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -115,6 +127,11 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
!i18n.resolvedLanguage || !(i18n.resolvedLanguage in localesBNAI)
|
||||
? DEFAULT_LOCALE
|
||||
: i18n.resolvedLanguage;
|
||||
// The math and diagram blocks ship the same set of locales.
|
||||
const langLocalesBNMathDiagram =
|
||||
!i18n.resolvedLanguage || !(i18n.resolvedLanguage in mathLocales)
|
||||
? DEFAULT_LOCALE
|
||||
: i18n.resolvedLanguage;
|
||||
|
||||
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
|
||||
const conf = useConfig().data;
|
||||
@@ -205,6 +222,11 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
},
|
||||
dictionary: {
|
||||
...localesBN[langLocalesBN as keyof typeof localesBN],
|
||||
math: mathLocales[langLocalesBNMathDiagram as keyof typeof mathLocales],
|
||||
diagram:
|
||||
diagramLocales[
|
||||
langLocalesBNMathDiagram as keyof typeof diagramLocales
|
||||
],
|
||||
...(localesBNMultiColumn && {
|
||||
multi_column:
|
||||
localesBNMultiColumn[
|
||||
@@ -234,6 +256,9 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
return defaultPasteHandler();
|
||||
},
|
||||
extensions: [
|
||||
// Highlights the source of code blocks and of the math / diagram
|
||||
// blocks' editable LaTeX / Mermaid popups.
|
||||
syntaxHighlighter,
|
||||
CommentsExtension({ threadStore, resolveUsers }),
|
||||
...(aiExtension ? [aiExtension] : []),
|
||||
],
|
||||
@@ -265,6 +290,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
langLocalesBN,
|
||||
langLocalesBNMultiColumn,
|
||||
langLocalesBNAI,
|
||||
langLocalesBNMathDiagram,
|
||||
provider,
|
||||
uploadFile,
|
||||
threadStore,
|
||||
|
||||
+4
@@ -1,5 +1,7 @@
|
||||
import { combineByGroup } from '@blocknote/core';
|
||||
import { filterSuggestionItems } from '@blocknote/core/extensions';
|
||||
import { getDiagramSlashMenuItems } from '@blocknote/diagram-block';
|
||||
import { getMathSlashMenuItems } from '@blocknote/math-block';
|
||||
import {
|
||||
DefaultReactSuggestionItem,
|
||||
SuggestionMenuController,
|
||||
@@ -57,6 +59,8 @@ export const BlockNoteSuggestionMenu = ({
|
||||
getMultiColumnSlashMenuItems?.(editor) || [],
|
||||
getPdfReactSlashMenuItems(editor, t, fileBlocksName),
|
||||
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
|
||||
getMathSlashMenuItems(editor),
|
||||
getDiagramSlashMenuItems(editor),
|
||||
aiAllowed && getAISlashMenuItems ? getAISlashMenuItems(editor) : [],
|
||||
);
|
||||
|
||||
|
||||
+7
-1
@@ -1,8 +1,11 @@
|
||||
import { getDiagramBlockTypeSelectItems } from '@blocknote/diagram-block';
|
||||
import { getMathBlockTypeSelectItems } from '@blocknote/math-block';
|
||||
import {
|
||||
FormattingToolbar,
|
||||
FormattingToolbarController,
|
||||
blockTypeSelectItems,
|
||||
getFormattingToolbarItems,
|
||||
useBlockNoteEditor,
|
||||
useDictionary,
|
||||
} from '@blocknote/react';
|
||||
import dynamic from 'next/dynamic';
|
||||
@@ -30,6 +33,7 @@ const ModalConfirmDownloadUnsafe = dynamic(
|
||||
const AIToolbarButton = BlockNoteAI?.AIToolbarButton;
|
||||
|
||||
export const BlockNoteToolbar = ({ aiAllowed }: { aiAllowed: boolean }) => {
|
||||
const editor = useBlockNoteEditor();
|
||||
const dict = useDictionary();
|
||||
const [confirmOpen, setIsConfirmOpen] = useState(false);
|
||||
const [onConfirm, setOnConfirm] = useState<() => void | Promise<void>>();
|
||||
@@ -40,6 +44,8 @@ export const BlockNoteToolbar = ({ aiAllowed }: { aiAllowed: boolean }) => {
|
||||
let toolbarItems = getFormattingToolbarItems([
|
||||
...blockTypeSelectItems(dict),
|
||||
getCalloutFormattingToolbarItems(t),
|
||||
...getMathBlockTypeSelectItems(editor),
|
||||
...getDiagramBlockTypeSelectItems(editor),
|
||||
]);
|
||||
|
||||
// Find the index of the file download button
|
||||
@@ -75,7 +81,7 @@ export const BlockNoteToolbar = ({ aiAllowed }: { aiAllowed: boolean }) => {
|
||||
});
|
||||
|
||||
return toolbarItems;
|
||||
}, [dict, t]);
|
||||
}, [dict, editor, t]);
|
||||
|
||||
const formattingToolbar = useCallback(() => {
|
||||
return (
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { diagramBlockMapping } from '@blocknote/diagram-block/docx-exporter';
|
||||
import {
|
||||
inlineMathMapping,
|
||||
mathBlockMapping,
|
||||
} from '@blocknote/math-block/docx-exporter';
|
||||
import { docxDefaultSchemaMappings } from '@blocknote/xl-docx-exporter';
|
||||
|
||||
import {
|
||||
@@ -18,6 +23,10 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
|
||||
// implementation signature, so we can reuse the handler directly.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
pdf: docxDefaultSchemaMappings.blockMapping.file as any,
|
||||
// Renders the LaTeX as a native (editable) Word equation.
|
||||
mathBlock: mathBlockMapping,
|
||||
// Renders the Mermaid source to a PNG in the browser (async mapping).
|
||||
diagram: diagramBlockMapping,
|
||||
quote: blockMappingQuoteDocx,
|
||||
image: blockMappingImageDocx,
|
||||
uploadLoader: blockMappingUploadLoaderDocx,
|
||||
@@ -48,6 +57,8 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
|
||||
inlineContentMapping: {
|
||||
...docxDefaultSchemaMappings.inlineContentMapping,
|
||||
interlinkingLinkInline: inlineContentMappingInterlinkingLinkDocx,
|
||||
// Renders inline math as a native (editable) Word equation.
|
||||
math: inlineMathMapping,
|
||||
},
|
||||
styleMapping: {
|
||||
...docxDefaultSchemaMappings.styleMapping,
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { diagramBlockMapping } from '@blocknote/diagram-block/odt-exporter';
|
||||
import {
|
||||
inlineMathMapping,
|
||||
mathBlockMapping,
|
||||
} from '@blocknote/math-block/odt-exporter';
|
||||
import { odtDefaultSchemaMappings } from '@blocknote/xl-odt-exporter';
|
||||
|
||||
import {
|
||||
@@ -23,10 +28,16 @@ export const odtDocsSchemaMappings: DocsExporterODT['mappings'] = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
pdf: odtDefaultSchemaMappings.blockMapping.file as any,
|
||||
uploadLoader: blockMappingUploadLoaderODT,
|
||||
// Renders the LaTeX as a native (editable) ODF formula object.
|
||||
mathBlock: mathBlockMapping,
|
||||
// Renders the Mermaid source to a PNG in the browser (async mapping).
|
||||
diagram: diagramBlockMapping,
|
||||
},
|
||||
|
||||
inlineContentMapping: {
|
||||
...baseInlineMappings,
|
||||
interlinkingLinkInline: inlineContentMappingInterlinkingLinkODT,
|
||||
// Renders inline math as a native (editable) ODF formula object.
|
||||
math: inlineMathMapping,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { diagramBlockMapping } from '@blocknote/diagram-block/pdf-exporter';
|
||||
import {
|
||||
inlineMathMapping,
|
||||
mathBlockMapping,
|
||||
} from '@blocknote/math-block/pdf-exporter';
|
||||
import { pdfDefaultSchemaMappings } from '@blocknote/xl-pdf-exporter';
|
||||
|
||||
import {
|
||||
@@ -27,10 +32,16 @@ export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
pdf: pdfDefaultSchemaMappings.blockMapping.file as any,
|
||||
uploadLoader: blockMappingUploadLoaderPDF,
|
||||
// Renders the LaTeX as a vector formula (via @react-pdf/math).
|
||||
mathBlock: mathBlockMapping,
|
||||
// Renders the Mermaid source to a PNG in the browser (async mapping).
|
||||
diagram: diagramBlockMapping,
|
||||
},
|
||||
inlineContentMapping: {
|
||||
...pdfDefaultSchemaMappings.inlineContentMapping,
|
||||
interlinkingLinkInline: inlineContentMappingInterlinkingLinkPDF,
|
||||
// Inline math is rasterized to an image that flows with the text.
|
||||
math: inlineMathMapping,
|
||||
},
|
||||
styleMapping: {
|
||||
...pdfDefaultSchemaMappings.styleMapping,
|
||||
|
||||
@@ -30,8 +30,8 @@ const PRINT_ONLY_CONTENT_CSS = `
|
||||
div[data-is-empty-and-focused="true"],
|
||||
div[data-floating-ui-focusable],
|
||||
.collaboration-cursor-custom__base,
|
||||
.c__toast__container
|
||||
{
|
||||
.bn-source-block-popup,
|
||||
.c__toast__container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,11 @@ const extractInlineText = (content: PresenterBlock['content']): string => {
|
||||
return node.text;
|
||||
}
|
||||
if ('content' in node && node.content !== undefined) {
|
||||
return extractInlineText(node.content);
|
||||
// Plain inline content (e.g. inline math) exposes its content as a
|
||||
// raw string rather than a list of inline nodes.
|
||||
return typeof node.content === 'string'
|
||||
? node.content
|
||||
: extractInlineText(node.content);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
|
||||
+832
-8
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user