Skip to content

Commit 0ead877

Browse files
ThomasMetzgerSimonClo
authored andcommitted
✨(frontend) embed-block: added url embed block
1 parent e504f43 commit 0ead877

File tree

8 files changed

+184
-1
lines changed

8 files changed

+184
-1
lines changed

src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ import { randomColor } from '../utils';
2727

2828
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
2929
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
30-
import { CalloutBlock, DividerBlock } from './custom-blocks';
30+
import { CalloutBlock, DividerBlock, ReactEmbedBlock } from './custom-blocks';
3131

3232
export const blockNoteSchema = withPageBreak(
3333
BlockNoteSchema.create({
3434
blockSpecs: {
3535
...defaultBlockSpecs,
3636
callout: CalloutBlock,
3737
divider: DividerBlock,
38+
embed: ReactEmbedBlock,
3839
},
3940
}),
4041
);

src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { DocsBlockSchema } from '../types';
1414
import {
1515
getCalloutReactSlashMenuItems,
1616
getDividerReactSlashMenuItems,
17+
getEmbedReactSlashMenuItems,
1718
} from './custom-blocks';
1819

1920
export const BlockNoteSuggestionMenu = () => {
@@ -30,6 +31,7 @@ export const BlockNoteSuggestionMenu = () => {
3031
getPageBreakReactSlashMenuItems(editor),
3132
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
3233
getDividerReactSlashMenuItems(editor, t, basicBlocksName),
34+
getEmbedReactSlashMenuItems(editor, t, basicBlocksName),
3335
),
3436
query,
3537
),
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import {
2+
FileBlockConfig,
3+
InlineContentSchema,
4+
PropSchema,
5+
StyleSchema,
6+
insertOrUpdateBlock,
7+
} from '@blocknote/core';
8+
import {
9+
BlockTypeSelectItem,
10+
ReactCustomBlockRenderProps,
11+
ResizableFileBlockWrapper,
12+
createReactBlockSpec,
13+
} from '@blocknote/react';
14+
import { TFunction } from 'i18next';
15+
import React, { useState } from 'react';
16+
import { css } from 'styled-components';
17+
18+
import { Box, Icon } from '@/components';
19+
20+
import { DocsBlockNoteEditor } from '../../types';
21+
22+
export const iframePropSchema: PropSchema & {
23+
caption: {
24+
default: '';
25+
};
26+
name: {
27+
default: '';
28+
};
29+
} = {
30+
url: { default: '' },
31+
caption: { default: '' },
32+
name: { default: '' },
33+
showPreview: { default: true },
34+
previewWidth: { type: 'number', default: undefined },
35+
};
36+
37+
export const iframeBlockConfig = {
38+
type: 'embed' as const,
39+
propSchema: iframePropSchema,
40+
content: 'none',
41+
isFileBlock: true,
42+
fileBlockAccept: ['image/png'],
43+
} satisfies FileBlockConfig;
44+
45+
export const IFrameViewer = (
46+
props: ReactCustomBlockRenderProps<
47+
typeof iframeBlockConfig,
48+
InlineContentSchema,
49+
StyleSchema
50+
>,
51+
) => {
52+
const url = props.block.props.url;
53+
54+
const [iframeError, setIframeError] = useState(false);
55+
if (!url) {
56+
return <Box>No URL provided for embed.</Box>;
57+
}
58+
59+
return !iframeError ? (
60+
<iframe
61+
src={url}
62+
className="bn-visual-media"
63+
style={{
64+
height: '300px',
65+
}}
66+
allowFullScreen
67+
title="Embedded content"
68+
onError={() => setIframeError(true)}
69+
/>
70+
) : (
71+
<Box
72+
$css={css`
73+
color: #d32f2f;
74+
background: #fff3f3;
75+
border: 1px solid #f8d7da;
76+
border-radius: 6px;
77+
padding: 1rem;
78+
`}
79+
>
80+
<Icon iconName="error" $size="16px" /> This site cannot be embedded. It
81+
may not allow embedding in an iframe.
82+
</Box>
83+
);
84+
};
85+
86+
export const IframeToExternalHTML = (
87+
props: ReactCustomBlockRenderProps<
88+
typeof iframeBlockConfig,
89+
InlineContentSchema,
90+
StyleSchema
91+
>,
92+
) => (
93+
<iframe
94+
src={props.block.props.url}
95+
className="bn-visual-media"
96+
style={{
97+
position: 'absolute',
98+
top: 0,
99+
left: 0,
100+
width: '100%',
101+
height: '100%',
102+
border: 'none',
103+
}}
104+
allowFullScreen
105+
title="Embedded content"
106+
/>
107+
);
108+
109+
export const IframeBlock = (
110+
props: ReactCustomBlockRenderProps<
111+
typeof iframeBlockConfig,
112+
InlineContentSchema,
113+
StyleSchema
114+
>,
115+
) => {
116+
return (
117+
<ResizableFileBlockWrapper
118+
{...(props as any)} // eslint-disable-line @typescript-eslint/no-explicit-any
119+
buttonText="Embed"
120+
buttonIcon={<Icon iconName="language" $size="18px" />}
121+
>
122+
<IFrameViewer {...props} />
123+
</ResizableFileBlockWrapper>
124+
);
125+
};
126+
127+
export const ReactEmbedBlock = createReactBlockSpec(iframeBlockConfig, {
128+
render: IframeBlock,
129+
parse: () => undefined,
130+
toExternalHTML: IframeToExternalHTML,
131+
});
132+
133+
export const getEmbedReactSlashMenuItems = (
134+
editor: DocsBlockNoteEditor,
135+
t: TFunction<'translation', undefined>,
136+
group: string,
137+
) => [
138+
{
139+
title: t('Embed'),
140+
onItemClick: () => {
141+
insertOrUpdateBlock(editor, {
142+
type: 'embed',
143+
});
144+
},
145+
aliases: ['embed', 'iframe', 'link'],
146+
group,
147+
icon: <Icon iconName="link" $size="18px" />,
148+
subtext: t('Add an embed block'),
149+
},
150+
];
151+
152+
export const getEmbedFormattingToolbarItems = (
153+
t: TFunction<'translation', undefined>,
154+
): BlockTypeSelectItem => ({
155+
name: t('Embed'),
156+
type: 'embed',
157+
icon: () => <Icon iconName="link" $size="16px" />,
158+
isSelected: (block: any) => block.type === 'embed',
159+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './CalloutBlock';
22
export * from './DividerBlock';
3+
export * from './EmbedBlock';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Paragraph } from 'docx';
2+
3+
import { DocsExporterDocx } from '../types';
4+
5+
export const blockMappingEmbedDocx: DocsExporterDocx['mappings']['blockMapping']['embed'] =
6+
() => {
7+
return new Paragraph({});
8+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Text } from '@react-pdf/renderer';
2+
3+
import { DocsExporterPDF } from '../types';
4+
5+
export const blockMappingEmbedPDF: DocsExporterPDF['mappings']['blockMapping']['embed'] =
6+
() => {
7+
return <Text />;
8+
};

src/frontend/apps/impress/src/features/docs/doc-export/mappingDocx.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
blockMappingQuoteDocx,
88
} from './blocks-mapping';
99
import { DocsExporterDocx } from './types';
10+
import { blockMappingEmbedDocx } from './blocks-mapping/embedDocx';
1011

1112
export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
1213
...docxDefaultSchemaMappings,
@@ -16,5 +17,6 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
1617
divider: blockMappingDividerDocx,
1718
quote: blockMappingQuoteDocx,
1819
image: blockMappingImageDocx,
20+
embed: blockMappingEmbedDocx,
1921
},
2022
};

src/frontend/apps/impress/src/features/docs/doc-export/mappingPDF.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
blockMappingTablePDF,
1111
} from './blocks-mapping';
1212
import { DocsExporterPDF } from './types';
13+
import { blockMappingEmbedPDF } from './blocks-mapping/embedPDF';
1314

1415
export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = {
1516
...pdfDefaultSchemaMappings,
@@ -22,5 +23,6 @@ export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = {
2223
divider: blockMappingDividerPDF,
2324
quote: blockMappingQuotePDF,
2425
table: blockMappingTablePDF,
26+
embed: blockMappingEmbedPDF,
2527
},
2628
};

0 commit comments

Comments
 (0)