Skip to content

✨(frontend) add Alert, Quote, and Divider blocks to the editor #566

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to

## Added
- 📝(doc) Add security.md and codeofconduct.md #604
- ✨(frontend) add Alert, Quote, and Divider blocks to the editor #566


## [2.1.0] - 2025-01-29

Expand Down
80 changes: 80 additions & 0 deletions src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,4 +368,84 @@ 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();
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Dictionary, locales } from '@blocknote/core';
import {
BlockNoteEditor as BlockNoteEditorCore,
BlockNoteSchema,
Dictionary,
defaultBlockSpecs,
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 { 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';
Expand All @@ -17,95 +22,27 @@ 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 { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
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;
}
}
`;
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;
Expand Down Expand Up @@ -164,6 +101,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
},
},
dictionary: locales[lang as keyof typeof locales] as Dictionary,
schema,
uploadFile,
},
[collabName, lang, provider, uploadFile],
Expand Down Expand Up @@ -198,8 +136,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
editor={editor}
formattingToolbar={false}
editable={!readOnly}
slashMenu={false}
theme="light"
>
<BlockNoteSuggestionMenu />
<BlockNoteToolbar />
</BlockNoteView>
</Box>
Expand All @@ -225,6 +165,7 @@ export const BlockNoteEditorVersion = ({
},
provider: undefined,
},
schema,
},
[initialContent],
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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}
/>
);
};
Loading
Loading