Skip to content

Commit c39dc5e

Browse files
committed
test(plugin-table): pin pasting over a rectangular selection
Paste over a rectangle needed no plugin code: `clipboard.paste` on an expanded selection decomposes into `delete` plus the forwarded paste, the plugin's clear intercepts the delete, and the select-time container repair leaves the caret on the minted leaf, so the pasted content lands in the top-left cell while the other member cells clear and cells outside the rectangle stay untouched. A second pin covers multi-block paste: both paragraphs land inside the cell and the table is not split. `deserialize` and `deserialize.data` flip to `pass` in the classification map: they are MIME negotiation only, and the selection consumer is `insert.blocks`, which stays honestly `pending` since its expanded-selection handling (auto-placement splitting with an addressed `delete`) is not verified against rectangles.
1 parent 26414cc commit c39dc5e

2 files changed

Lines changed: 101 additions & 4 deletions

File tree

packages/plugin-table/src/behaviors/classification.test.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,20 @@ const treatments: Record<
4242
'delete.child': 'pass',
4343
'delete.forward': 'pass',
4444
'delete.text': 'pending',
45-
// Paste over a rectangle.
46-
'deserialize': 'pending',
47-
'deserialize.data': 'pending',
45+
// MIME negotiation only; the application is `insert.blocks`, classified
46+
// separately. The paste gesture itself is safe: `clipboard.paste`
47+
// decomposes into `delete` (remapped) plus the forwarded paste, pinned in
48+
// delete.test.tsx.
49+
'deserialize': 'pass',
50+
'deserialize.data': 'pass',
4851
// Applies through `insert.blocks`, which is classified separately.
4952
'deserialization.failure': 'pass',
5053
'deserialization.success': 'pass',
5154
'history.redo': 'pass',
5255
'history.undo': 'pass',
5356
'insert': 'pass',
54-
// Typing or inserting over a rectangle should replace the rectangle.
57+
// Inserting over a rectangle should replace the rectangle; these are not
58+
// yet verified to decompose through `delete`.
5559
'insert.block': 'pending',
5660
'insert.blocks': 'pending',
5761
'insert.break': 'pending',

packages/plugin-table/src/behaviors/delete.test.tsx

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,99 @@ describe('delete behaviors within tables', () => {
553553
})
554554
})
555555

556+
test('pasting over a column selection replaces the column only', async () => {
557+
const {editor} = await createTestEditor({
558+
keyGenerator: createTestKeyGenerator(),
559+
schemaDefinition,
560+
initialValue,
561+
children: <TablePlugin />,
562+
})
563+
564+
const anchor = pointInSpan('r0c0', 'r0c0b', 'r0c0s', 0)
565+
const focus = pointInSpan('r1c0', 'r1c0b', 'r1c0s', 2)
566+
editor.send({type: 'select', at: {anchor, focus}})
567+
568+
// `clipboard.paste` on an expanded selection decomposes into `delete`
569+
// plus the forwarded paste, so the rectangle clears and the pasted
570+
// content lands in the top-left cell's minted block.
571+
const dataTransfer = new DataTransfer()
572+
dataTransfer.setData('text/plain', 'X')
573+
editor.send({
574+
type: 'clipboard.paste',
575+
originEvent: {dataTransfer},
576+
position: {
577+
selection: {anchor, focus},
578+
},
579+
})
580+
581+
await vi.waitFor(() => {
582+
expect(editor.getSnapshot().context.value).toEqual([
583+
{
584+
_type: 'table',
585+
_key: 't0',
586+
rows: [
587+
{
588+
_type: 'row',
589+
_key: 'r0',
590+
cells: [
591+
cellWithText('r0c0', 'k2', 'k3', 'X'),
592+
cellWithText('r0c1', 'r0c1b', 'r0c1s', 'BB'),
593+
],
594+
},
595+
{
596+
_type: 'row',
597+
_key: 'r1',
598+
cells: [
599+
cellWithText('r1c0', 'k4', 'k5', ''),
600+
cellWithText('r1c1', 'r1c1b', 'r1c1s', 'DD'),
601+
],
602+
},
603+
],
604+
},
605+
])
606+
})
607+
608+
editor.send({type: 'history.undo'})
609+
610+
await vi.waitFor(() => {
611+
expect(editor.getSnapshot().context.value).toEqual(initialValue)
612+
})
613+
})
614+
615+
test('pasting multi-line text over a rectangle lands inside the cell', async () => {
616+
const {editor} = await createTestEditor({
617+
keyGenerator: createTestKeyGenerator(),
618+
schemaDefinition,
619+
initialValue,
620+
children: <TablePlugin />,
621+
})
622+
623+
const anchor = pointInSpan('r0c0', 'r0c0b', 'r0c0s', 0)
624+
const focus = pointInSpan('r1c0', 'r1c0b', 'r1c0s', 2)
625+
editor.send({type: 'select', at: {anchor, focus}})
626+
627+
const dataTransfer = new DataTransfer()
628+
// A blank line separates paragraphs; a single newline is a soft break.
629+
dataTransfer.setData('text/plain', 'one\n\ntwo')
630+
editor.send({
631+
type: 'clipboard.paste',
632+
originEvent: {dataTransfer},
633+
position: {
634+
selection: {anchor, focus},
635+
},
636+
})
637+
638+
await vi.waitFor(() => {
639+
const value = editor.getSnapshot().context.value as typeof initialValue
640+
const anchorCell = value[0]?.rows[0]?.cells[0]
641+
// Both pasted blocks belong to the cell; the table is not split.
642+
expect(value).toHaveLength(1)
643+
expect(anchorCell?.value.map((block) => block.children[0]?.text)).toEqual(
644+
['one', 'two'],
645+
)
646+
})
647+
})
648+
556649
test('delete.range straddling two cells in same row: structure preserved (no cell merge)', async () => {
557650
const {editor} = await createTestEditor({
558651
keyGenerator: createTestKeyGenerator(),

0 commit comments

Comments
 (0)