Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0815af4977 | |||
| bf0b1591b3 |
@@ -11,6 +11,8 @@ and this project adheres to
|
|||||||
|
|
||||||
## Added
|
## Added
|
||||||
- 📝(doc) Add security.md and codeofconduct.md #604
|
- 📝(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
|
## [2.1.0] - 2025-01-29
|
||||||
|
|
||||||
|
|||||||
@@ -368,4 +368,84 @@ test.describe('Doc Editor', () => {
|
|||||||
|
|
||||||
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+27
-86
@@ -1,4 +1,10 @@
|
|||||||
import { Dictionary, locales } from '@blocknote/core';
|
import {
|
||||||
|
BlockNoteEditor as BlockNoteEditorCore,
|
||||||
|
BlockNoteSchema,
|
||||||
|
Dictionary,
|
||||||
|
defaultBlockSpecs,
|
||||||
|
locales,
|
||||||
|
} from '@blocknote/core';
|
||||||
import '@blocknote/core/fonts/inter.css';
|
import '@blocknote/core/fonts/inter.css';
|
||||||
import { BlockNoteView } from '@blocknote/mantine';
|
import { BlockNoteView } from '@blocknote/mantine';
|
||||||
import '@blocknote/mantine/style.css';
|
import '@blocknote/mantine/style.css';
|
||||||
@@ -6,7 +12,6 @@ import { useCreateBlockNote } from '@blocknote/react';
|
|||||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import { Box, TextErrors } from '@/components';
|
import { Box, TextErrors } from '@/components';
|
||||||
@@ -17,95 +22,27 @@ import { useUploadFile } from '../hook';
|
|||||||
import { useHeadings } from '../hook/useHeadings';
|
import { useHeadings } from '../hook/useHeadings';
|
||||||
import useSaveDoc from '../hook/useSaveDoc';
|
import useSaveDoc from '../hook/useSaveDoc';
|
||||||
import { useEditorStore } from '../stores';
|
import { useEditorStore } from '../stores';
|
||||||
|
import { cssEditor } from '../styles';
|
||||||
import { randomColor } from '../utils';
|
import { randomColor } from '../utils';
|
||||||
|
|
||||||
|
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
|
||||||
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||||
|
import { AlertBlock, DividerBlock, QuoteBlock } from './custom-blocks';
|
||||||
|
|
||||||
const cssEditor = (readonly: boolean) => css`
|
export const schema = BlockNoteSchema.create({
|
||||||
&,
|
blockSpecs: {
|
||||||
& > .bn-container,
|
...defaultBlockSpecs,
|
||||||
& .ProseMirror {
|
alert: AlertBlock,
|
||||||
height: 100%;
|
quote: QuoteBlock,
|
||||||
|
divider: DividerBlock,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
.bn-side-menu[data-block-type='heading'][data-level='1'] {
|
export type DocsBlockNoteEditor = BlockNoteEditorCore<
|
||||||
height: 50px;
|
typeof schema.blockSchema,
|
||||||
}
|
typeof schema.inlineContentSchema,
|
||||||
.bn-side-menu[data-block-type='heading'][data-level='2'] {
|
typeof schema.styleSchema
|
||||||
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 {
|
interface BlockNoteEditorProps {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
@@ -164,6 +101,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
dictionary: locales[lang as keyof typeof locales] as Dictionary,
|
dictionary: locales[lang as keyof typeof locales] as Dictionary,
|
||||||
|
schema,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
},
|
},
|
||||||
[collabName, lang, provider, uploadFile],
|
[collabName, lang, provider, uploadFile],
|
||||||
@@ -198,8 +136,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
editor={editor}
|
editor={editor}
|
||||||
formattingToolbar={false}
|
formattingToolbar={false}
|
||||||
editable={!readOnly}
|
editable={!readOnly}
|
||||||
|
slashMenu={false}
|
||||||
theme="light"
|
theme="light"
|
||||||
>
|
>
|
||||||
|
<BlockNoteSuggestionMenu />
|
||||||
<BlockNoteToolbar />
|
<BlockNoteToolbar />
|
||||||
</BlockNoteView>
|
</BlockNoteView>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -225,6 +165,7 @@ export const BlockNoteEditorVersion = ({
|
|||||||
},
|
},
|
||||||
provider: undefined,
|
provider: undefined,
|
||||||
},
|
},
|
||||||
|
schema,
|
||||||
},
|
},
|
||||||
[initialContent],
|
[initialContent],
|
||||||
);
|
);
|
||||||
|
|||||||
+39
@@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
/* 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
@@ -0,0 +1,55 @@
|
|||||||
|
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
@@ -0,0 +1,63 @@
|
|||||||
|
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
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './AlertBlock';
|
||||||
|
export * from './DividerBlock';
|
||||||
|
export * from './QuoteBlock';
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { BlockNoteEditor } from '@blocknote/core';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { DocsBlockNoteEditor } from '../components/BlockNoteEditor';
|
||||||
import { useHeadingStore } from '../stores';
|
import { useHeadingStore } from '../stores';
|
||||||
|
|
||||||
export const useHeadings = (editor: BlockNoteEditor) => {
|
export const useHeadings = (editor: DocsBlockNoteEditor) => {
|
||||||
const { setHeadings, resetHeadings } = useHeadingStore();
|
const { setHeadings, resetHeadings } = useHeadingStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { BlockNoteEditor } from '@blocknote/core';
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { DocsBlockNoteEditor } from '../components/BlockNoteEditor';
|
||||||
|
|
||||||
export interface UseEditorstore {
|
export interface UseEditorstore {
|
||||||
editor?: BlockNoteEditor;
|
editor?: DocsBlockNoteEditor;
|
||||||
setEditor: (editor: BlockNoteEditor | undefined) => void;
|
setEditor: (editor: DocsBlockNoteEditor | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useEditorStore = create<UseEditorstore>((set) => ({
|
export const useEditorStore = create<UseEditorstore>((set) => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BlockNoteEditor } from '@blocknote/core';
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { DocsBlockNoteEditor } from '../components/BlockNoteEditor';
|
||||||
import { HeadingBlock } from '../types';
|
import { HeadingBlock } from '../types';
|
||||||
|
|
||||||
const recursiveTextContent = (content: HeadingBlock['content']): string => {
|
const recursiveTextContent = (content: HeadingBlock['content']): string => {
|
||||||
@@ -21,7 +21,7 @@ const recursiveTextContent = (content: HeadingBlock['content']): string => {
|
|||||||
|
|
||||||
export interface UseHeadingStore {
|
export interface UseHeadingStore {
|
||||||
headings: HeadingBlock[];
|
headings: HeadingBlock[];
|
||||||
setHeadings: (editor: BlockNoteEditor) => void;
|
setHeadings: (editor: DocsBlockNoteEditor) => void;
|
||||||
resetHeadings: () => void;
|
resetHeadings: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
|
export const cssEditor = (readonly: boolean) => css`
|
||||||
|
&,
|
||||||
|
& > .bn-container,
|
||||||
|
& .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;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
+3
-2
@@ -1,10 +1,11 @@
|
|||||||
import { BlockNoteEditor } from '@blocknote/core';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { BoxButton, Text } from '@/components';
|
import { BoxButton, Text } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
|
import { DocsBlockNoteEditor } from '../../doc-editor/components/BlockNoteEditor';
|
||||||
|
|
||||||
const leftPaddingMap: { [key: number]: string } = {
|
const leftPaddingMap: { [key: number]: string } = {
|
||||||
3: '1.5rem',
|
3: '1.5rem',
|
||||||
2: '0.9rem',
|
2: '0.9rem',
|
||||||
@@ -17,7 +18,7 @@ export type HeadingsHighlight = {
|
|||||||
}[];
|
}[];
|
||||||
|
|
||||||
interface HeadingProps {
|
interface HeadingProps {
|
||||||
editor: BlockNoteEditor;
|
editor: DocsBlockNoteEditor;
|
||||||
level: number;
|
level: number;
|
||||||
text: string;
|
text: string;
|
||||||
headingId: string;
|
headingId: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user