From 0005fbdff59a57682fdb39426b14238072b1b6ac Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 2 Jan 2025 17:09:02 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20multi=20columns=20s?= =?UTF-8?q?upport=20for=20editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We add multi columns support for editor, now you can add columns to your document. --- .../__tests__/app-impress/doc-editor.spec.ts | 23 +++++++++ src/frontend/apps/impress/package.json | 1 + .../doc-editor/components/BlockNoteEditor.tsx | 48 +++++++++++++++++-- .../docs/doc-editor/hook/useHeadings.tsx | 4 +- .../docs/doc-editor/stores/useEditorStore.tsx | 7 +-- .../doc-editor/stores/useHeadingStore.tsx | 5 +- .../src/features/docs/doc-editor/types.tsx | 10 ++++ .../src/features/docs/doc-editor/utils.ts | 7 +++ .../doc-table-content/components/Heading.tsx | 4 +- src/frontend/yarn.lock | 31 ++++++++---- 10 files changed, 117 insertions(+), 23 deletions(-) diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts index af5b82204..7ec7852f6 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts @@ -351,4 +351,27 @@ test.describe('Doc Editor', () => { await expect(editor.getByText('Bonjour le monde')).toBeVisible(); }); + + test('it checks the multi columns', async ({ page, browserName }) => { + await createDoc(page, 'doc-multi-columns', browserName, 1); + + await page.locator('.bn-block-outer').last().fill('/'); + + await page.getByText('Three Columns', { exact: true }).click(); + + await page.locator('.bn-block-column').first().fill('Column 1'); + await page.locator('.bn-block-column').nth(1).fill('Column 2'); + await page.locator('.bn-block-column').last().fill('Column 3'); + + expect(await page.locator('.bn-block-column').count()).toBe(3); + await expect( + page.locator('.bn-block-column[data-node-type="column"]').first(), + ).toHaveText('Column 1'); + await expect( + page.locator('.bn-block-column[data-node-type="column"]').nth(1), + ).toHaveText('Column 2'); + await expect( + page.locator('.bn-block-column[data-node-type="column"]').last(), + ).toHaveText('Column 3'); + }); }); diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json index e70dcc3c9..a704aaeb4 100644 --- a/src/frontend/apps/impress/package.json +++ b/src/frontend/apps/impress/package.json @@ -18,6 +18,7 @@ "@blocknote/core": "0.22.0", "@blocknote/mantine": "0.22.0", "@blocknote/react": "0.22.0", + "@blocknote/xl-multi-column": "0.22.0", "@gouvfr-lasuite/integration": "1.0.2", "@hocuspocus/provider": "2.15.0", "@openfun/cunningham-react": "2.9.4", 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 868c5a6d2..ffae7fcb5 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 @@ -1,10 +1,24 @@ -import { Dictionary, locales } from '@blocknote/core'; +import { + Dictionary, + combineByGroup, + filterSuggestionItems, + locales, +} from '@blocknote/core'; import '@blocknote/core/fonts/inter.css'; import { BlockNoteView } from '@blocknote/mantine'; import '@blocknote/mantine/style.css'; -import { useCreateBlockNote } from '@blocknote/react'; +import { + SuggestionMenuController, + getDefaultReactSlashMenuItems, + useCreateBlockNote, +} from '@blocknote/react'; +import { + getMultiColumnSlashMenuItems, + multiColumnDropCursor, + locales as multiColumnLocales, +} from '@blocknote/xl-multi-column'; import { HocuspocusProvider } from '@hocuspocus/provider'; -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import * as Y from 'yjs'; @@ -16,7 +30,7 @@ import { useUploadFile } from '../hook'; import { useHeadings } from '../hook/useHeadings'; import useSaveDoc from '../hook/useSaveDoc'; import { useEditorStore } from '../stores'; -import { randomColor } from '../utils'; +import { blockNoteWithMultiColumn, randomColor } from '../utils'; import { BlockNoteToolbar } from './BlockNoteToolbar'; @@ -120,8 +134,14 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { return cursor; }, }, - dictionary: locales[lang as keyof typeof locales] as Dictionary, + dictionary: { + ...(locales[lang as keyof typeof locales] as Dictionary), + multi_column: + multiColumnLocales[lang as keyof typeof multiColumnLocales], + }, uploadFile, + schema: blockNoteWithMultiColumn, + dropCursor: multiColumnDropCursor, }, [collabName, lang, provider, uploadFile], ); @@ -152,6 +172,18 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { }; }, [setEditor, editor]); + const getSlashMenuItems = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/require-await + return async (query: string) => + filterSuggestionItems( + combineByGroup( + getDefaultReactSlashMenuItems(editor), + getMultiColumnSlashMenuItems(editor), + ), + query, + ); + }, [editor]); + return ( {errorAttachment && ( @@ -169,7 +201,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { formattingToolbar={false} editable={!readOnly} theme="light" + slashMenu={false} > + @@ -195,6 +232,7 @@ export const BlockNoteEditorVersion = ({ }, provider: undefined, }, + schema: blockNoteWithMultiColumn, }, [initialContent], ); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx index 9468a7963..8b88eb3d6 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx @@ -1,9 +1,9 @@ -import { BlockNoteEditor } from '@blocknote/core'; import { useEffect } from 'react'; import { useHeadingStore } from '../stores'; +import { DocsBlockNoteEditor } from '../types'; -export const useHeadings = (editor: BlockNoteEditor) => { +export const useHeadings = (editor: DocsBlockNoteEditor) => { const { setHeadings, resetHeadings } = useHeadingStore(); useEffect(() => { diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useEditorStore.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useEditorStore.tsx index 025f2ad8e..9a846b37c 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useEditorStore.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useEditorStore.tsx @@ -1,9 +1,10 @@ -import { BlockNoteEditor } from '@blocknote/core'; import { create } from 'zustand'; +import { DocsBlockNoteEditor } from '../types'; + export interface UseEditorstore { - editor?: BlockNoteEditor; - setEditor: (editor: BlockNoteEditor | undefined) => void; + editor?: DocsBlockNoteEditor; + setEditor: (editor: DocsBlockNoteEditor | undefined) => void; } export const useEditorStore = create((set) => ({ diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useHeadingStore.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useHeadingStore.tsx index ac9b8a4b3..e67800c6c 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useHeadingStore.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/stores/useHeadingStore.tsx @@ -1,7 +1,6 @@ -import { BlockNoteEditor } from '@blocknote/core'; import { create } from 'zustand'; -import { HeadingBlock } from '../types'; +import { DocsBlockNoteEditor, HeadingBlock } from '../types'; const recursiveTextContent = (content: HeadingBlock['content']): string => { if (!content) { @@ -21,7 +20,7 @@ const recursiveTextContent = (content: HeadingBlock['content']): string => { export interface UseHeadingStore { headings: HeadingBlock[]; - setHeadings: (editor: BlockNoteEditor) => void; + setHeadings: (editor: DocsBlockNoteEditor) => void; resetHeadings: () => void; } 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 19094b6c5..634f85c61 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,7 @@ +import { BlockNoteEditor } from '@blocknote/core'; + +import { blockNoteWithMultiColumn } from './utils'; + export interface DocAttachment { file: string; } @@ -12,3 +16,9 @@ export type HeadingBlock = { level: number; }; }; + +export type DocsBlockNoteEditor = BlockNoteEditor< + typeof blockNoteWithMultiColumn.blockSchema, + typeof blockNoteWithMultiColumn.inlineContentSchema, + typeof blockNoteWithMultiColumn.styleSchema +>; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts index a3d311180..15b4e87b2 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts @@ -1,3 +1,6 @@ +import { BlockNoteSchema } from '@blocknote/core'; +import { withMultiColumn } from '@blocknote/xl-multi-column'; + export const randomColor = () => { const randomInt = (min: number, max: number) => { return Math.floor(Math.random() * (max - min + 1)) + min; @@ -25,3 +28,7 @@ function hslToHex(h: number, s: number, l: number) { export const toBase64 = (str: Uint8Array) => Buffer.from(str).toString('base64'); + +export const blockNoteWithMultiColumn = withMultiColumn( + BlockNoteSchema.create(), +); diff --git a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx index 5e92efa5c..28509d8b6 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx @@ -1,8 +1,8 @@ -import { BlockNoteEditor } from '@blocknote/core'; import { useState } from 'react'; import { BoxButton, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; +import { DocsBlockNoteEditor } from '@/features/docs/doc-editor'; import { useResponsiveStore } from '@/stores'; const sizeMap: { [key: number]: string } = { @@ -17,7 +17,7 @@ export type HeadingsHighlight = { }[]; interface HeadingProps { - editor: BlockNoteEditor; + editor: DocsBlockNoteEditor; level: number; text: string; headingId: string; diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index f44320b62..294e82b31 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -1075,6 +1075,21 @@ y-protocols "^1.0.6" yjs "^13.6.15" +"@blocknote/xl-multi-column@^0.22.0": + version "0.22.0" + resolved "https://registry.yarnpkg.com/@blocknote/xl-multi-column/-/xl-multi-column-0.22.0.tgz#495a4dc4080c3fb7b2a4d436653502d1abd1d3f6" + integrity sha512-RcrCbH3VPGojB+R5gFLKQ48c2nnr0AqoiAsdpR3w13FvynRY4qVjq3ZVzpHPfKFs0/2D0YNxhQu4fny9roaazQ== + dependencies: + "@blocknote/core" "^0.22.0" + "@blocknote/react" "^0.22.0" + "@tiptap/core" "^2.7.1" + prosemirror-model "^1.23.0" + prosemirror-state "^1.4.3" + prosemirror-tables "^1.3.7" + prosemirror-transform "^1.9.0" + prosemirror-view "^1.33.7" + react-icons "^5.2.1" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -4736,7 +4751,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@22.10.3": +"@types/node@*": version "22.10.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.3.tgz#cdc2a89bf6e5d5e593fad08e83f74d7348d5dd10" integrity sha512-DifAyw4BkrufCILvD3ucnuN8eydUfc/C1GlyrnI+LK6543w5/L3VeVgf05o3B4fqSXP1dKYLOZsKfutpxPzZrw== @@ -4798,7 +4813,7 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== -"@types/react-dom@*", "@types/react-dom@18.3.1": +"@types/react-dom@*": version "18.3.1" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ== @@ -4943,7 +4958,7 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@*", "@typescript-eslint/eslint-plugin@8.19.0", "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": +"@typescript-eslint/eslint-plugin@*", "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": version "8.19.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz#2b1e1b791e21d5fc27ddc93884db066444f597b5" integrity sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q== @@ -4958,7 +4973,7 @@ natural-compare "^1.4.0" ts-api-utils "^1.3.0" -"@typescript-eslint/parser@*", "@typescript-eslint/parser@8.19.0", "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": +"@typescript-eslint/parser@*", "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": version "8.19.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.19.0.tgz#f1512e6e5c491b03aabb2718b95becde22b15292" integrity sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw== @@ -7048,7 +7063,7 @@ eslint-visitor-keys@^4.2.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== -eslint@*, eslint@8.57.0: +eslint@*: version "8.57.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== @@ -10923,7 +10938,7 @@ prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.3: prosemirror-transform "^1.0.0" prosemirror-view "^1.27.0" -prosemirror-tables@^1.6.1: +prosemirror-tables@^1.3.7, prosemirror-tables@^1.6.1: version "1.6.2" resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.6.2.tgz#cec9e9ac6ecf81d67147c19ab39125d56c8351ae" integrity sha512-97dKocVLrEVTQjZ4GBLdrrMw7Gv3no8H8yMwf5IRM9OoHrzbWpcH5jJxYgNQIRCtdIqwDctT1HdMHrGTiwp1dQ== @@ -10942,7 +10957,7 @@ prosemirror-trailing-node@^3.0.0: "@remirror/core-constants" "3.0.0" escape-string-regexp "^4.0.0" -prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.7.3: +prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.7.3, prosemirror-transform@^1.9.0: version "1.10.2" resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz#8ebac4e305b586cd96595aa028118c9191bbf052" integrity sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ== @@ -12930,7 +12945,7 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" -typescript@*, typescript@5.7.2, typescript@^5.0.4: +typescript@*, typescript@^5.0.4: version "5.7.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==