Skip to content

Commit 9704635

Browse files
authored
Improve editor paste handling (#358)
1 parent f7d933e commit 9704635

9 files changed

Lines changed: 937 additions & 24 deletions

File tree

src/components/editor/components/blocks/code/Code.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const CodeBlock = forwardRef<HTMLDivElement, EditorElementProps<CodeNode>
1414
({ node, children, ...attributes }, ref) => {
1515
const { language, handleChangeLanguage } = useCodeBlock(node);
1616
const [showToolbar, setShowToolbar] = useState(false);
17+
const isMermaid = language === 'mermaid';
1718

1819
const editor = useSlateStatic();
1920
const readOnly = useReadOnly() || editor.isElementReadOnly(node as unknown as Element);
@@ -52,10 +53,18 @@ export const CodeBlock = forwardRef<HTMLDivElement, EditorElementProps<CodeNode>
5253
<div {...attributes} ref={ref} className={`${attributes.className ?? ''} flex w-full`}>
5354
<pre
5455
spellCheck={false}
55-
className={`appflowy-scroller flex w-full flex-col overflow-auto rounded-[8px] border border-border-primary bg-fill-list-active p-5 pt-12`}
56+
className={`appflowy-scroller relative flex w-full flex-col overflow-auto rounded-[8px] border border-border-primary bg-fill-list-active p-5 pt-12`}
5657
>
57-
<code>{children}</code>
58-
{language === 'mermaid' && (
58+
<code
59+
className={
60+
isMermaid
61+
? 'pointer-events-none absolute h-px w-px overflow-hidden opacity-0'
62+
: undefined
63+
}
64+
>
65+
{children}
66+
</code>
67+
{isMermaid && (
5968
<Suspense>
6069
<MermaidChat node={node} />
6170
</Suspense>

src/components/editor/parsers/__tests__/block-converters.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ describe('block-converters', () => {
7979

8080
const block = parseParagraph(node);
8181

82+
if (Array.isArray(block)) throw new Error('Expected one paragraph block');
8283
expect(block.type).toBe(BlockType.Paragraph);
8384
expect(block.text).toBe('Simple paragraph text');
8485
expect(block.formats).toEqual([]);
@@ -109,6 +110,7 @@ describe('block-converters', () => {
109110

110111
const block = parseParagraph(node);
111112

113+
if (Array.isArray(block)) throw new Error('Expected one paragraph block');
112114
expect(block.text).toBe('Text with bold and italic');
113115
expect(block.formats).toHaveLength(2);
114116
});
@@ -131,13 +133,109 @@ describe('block-converters', () => {
131133

132134
const block = parseParagraph(node);
133135

136+
if (Array.isArray(block)) throw new Error('Expected one paragraph block');
134137
expect(block.text).toBe('Visit our site');
135138
expect(block.formats).toHaveLength(1);
136139
expect(block.formats[0]).toMatchObject({
137140
type: 'link',
138141
data: { href: 'https://example.com' },
139142
});
140143
});
144+
145+
it('should split br-separated clipboard lines into readable paragraphs', () => {
146+
const node: HastElement = {
147+
type: 'element',
148+
tagName: 'div',
149+
properties: {},
150+
children: [
151+
{ type: 'text', value: 'Overview' },
152+
{ type: 'element', tagName: 'br', properties: {}, children: [] },
153+
{ type: 'text', value: 'Desktop checklist' },
154+
{ type: 'element', tagName: 'br', properties: {}, children: [] },
155+
{ type: 'text', value: ' Mobile checklist' },
156+
],
157+
};
158+
159+
const blocks = parseParagraph(node);
160+
161+
expect(blocks).toEqual([
162+
expect.objectContaining({ type: BlockType.Paragraph, text: 'Overview' }),
163+
expect.objectContaining({ type: BlockType.Paragraph, text: 'Desktop checklist' }),
164+
expect.objectContaining({ type: BlockType.Paragraph, text: ' Mobile checklist' }),
165+
]);
166+
});
167+
168+
it('should split nested div clipboard lines into readable paragraphs', () => {
169+
const node: HastElement = {
170+
type: 'element',
171+
tagName: 'div',
172+
properties: {},
173+
children: [
174+
{
175+
type: 'element',
176+
tagName: 'div',
177+
properties: {},
178+
children: [{ type: 'text', value: 'Summary' }],
179+
},
180+
{
181+
type: 'element',
182+
tagName: 'div',
183+
properties: {},
184+
children: [{ type: 'text', value: 'Owner: Alex' }],
185+
},
186+
{
187+
type: 'element',
188+
tagName: 'div',
189+
properties: {},
190+
children: [{ type: 'text', value: 'Status: ready' }],
191+
},
192+
],
193+
};
194+
195+
const blocks = parseParagraph(node);
196+
197+
expect(blocks).toEqual([
198+
expect.objectContaining({ type: BlockType.Paragraph, text: 'Summary' }),
199+
expect.objectContaining({ type: BlockType.Paragraph, text: 'Owner: Alex' }),
200+
expect.objectContaining({ type: BlockType.Paragraph, text: 'Status: ready' }),
201+
]);
202+
});
203+
204+
it('should preserve inline formats when splitting clipboard lines', () => {
205+
const node: HastElement = {
206+
type: 'element',
207+
tagName: 'div',
208+
properties: {},
209+
children: [
210+
{
211+
type: 'element',
212+
tagName: 'strong',
213+
properties: {},
214+
children: [{ type: 'text', value: 'Important' }],
215+
},
216+
{ type: 'element', tagName: 'br', properties: {}, children: [] },
217+
{
218+
type: 'element',
219+
tagName: 'em',
220+
properties: {},
221+
children: [{ type: 'text', value: 'Follow up' }],
222+
},
223+
],
224+
};
225+
226+
const blocks = parseParagraph(node);
227+
228+
expect(blocks).toEqual([
229+
expect.objectContaining({
230+
text: 'Important',
231+
formats: [{ start: 0, end: 9, type: 'bold', data: undefined }],
232+
}),
233+
expect.objectContaining({
234+
text: 'Follow up',
235+
formats: [{ start: 0, end: 9, type: 'italic', data: undefined }],
236+
}),
237+
]);
238+
});
141239
});
142240

143241
describe('parseCodeBlock', () => {

0 commit comments

Comments
 (0)