diff --git a/packages/core/src/editor/transformPasted.ts b/packages/core/src/editor/transformPasted.ts index 42e45fd122..fa1ab7251a 100644 --- a/packages/core/src/editor/transformPasted.ts +++ b/packages/core/src/editor/transformPasted.ts @@ -2,6 +2,51 @@ import { Fragment, Schema, Slice } from "@tiptap/pm/model"; import { EditorView } from "@tiptap/pm/view"; import { getBlockInfoFromSelection } from "../api/getBlockInfoFromPos.js"; +import { findParentNodeClosestToPos } from "@tiptap/core"; + +/** + * Checks if the current selection is inside a table cell. + * Returns the depth of the tableCell/tableHeader node if found, -1 otherwise. + */ +function isInTableCell(view: EditorView): boolean { + return ( + findParentNodeClosestToPos(view.state.selection.$from, (n) => { + return n.type.name === "tableCell" || n.type.name === "tableHeader"; + }) !== undefined + ); +} + +/** + * Converts block content to inline content with hard breaks. + * This is used when pasting into table cells which can only contain inline content. + */ +function convertBlocksToInlineContent( + fragment: Fragment, + schema: Schema, +): Fragment { + const hardBreak = schema.nodes.hardBreak; + let flattenedFragment = Fragment.empty; + + fragment.forEach((node) => { + if (node.isInline) { + // This is a paragraph or similar - extract its inline content + flattenedFragment = flattenedFragment.append( + convertBlocksToInlineContent(node.content, schema), + ); + // Add hard break after each block (except we'll remove the last one) + flattenedFragment = flattenedFragment.addToEnd(hardBreak.create()); + } else if (node.isText) { + flattenedFragment = flattenedFragment.addToEnd(node); + } + }); + + // Remove the last hard break if present + if (flattenedFragment.lastChild?.type?.name === "hardBreak") { + flattenedFragment.cut(0, flattenedFragment.childCount - 1); + } + + return flattenedFragment; +} // helper function to remove a child from a fragment function removeChild(node: Fragment, n: number) { @@ -65,6 +110,25 @@ export function transformPasted(slice: Slice, view: EditorView) { let f = Fragment.from(slice.content); f = wrapTableRows(f, view.state.schema); + if (isInTableCell(view)) { + // If the pasted content has block-level elements, convert to inline content + let hasBlockContent = false; + f.descendants((node) => { + if (node.isInline && node.childCount > 0) { + // This is a paragraph with content + hasBlockContent = true; + } + }); + + if (hasBlockContent && f.childCount > 1) { + return new Slice( + convertBlocksToInlineContent(f, view.state.schema), + 0, + 0, + ); + } + } + if (!shouldApplyFix(f, view)) { // Don't apply the fix. return new Slice(f, slice.openStart, slice.openEnd); diff --git a/tests/src/unit/core/clipboard/paste/__snapshots__/text/html/pasteHTMLWithParagraphsInTableCell.json b/tests/src/unit/core/clipboard/paste/__snapshots__/text/html/pasteHTMLWithParagraphsInTableCell.json new file mode 100644 index 0000000000..60375f6ace --- /dev/null +++ b/tests/src/unit/core/clipboard/paste/__snapshots__/text/html/pasteHTMLWithParagraphsInTableCell.json @@ -0,0 +1,114 @@ +[ + { + "children": [], + "content": { + "columnWidths": [ + undefined, + ], + "headerCols": undefined, + "headerRows": undefined, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 1Paragraph 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "1", + "props": { + "textColor": "default", + }, + "type": "table", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 2", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Paragraph 3", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": { + "columnWidths": [ + undefined, + ], + "headerCols": undefined, + "headerRows": undefined, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "4", + "props": { + "textColor": "default", + }, + "type": "table", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/clipboard/paste/__snapshots__/text/plain/pasteHTMLWithParagraphsInTableCell.json b/tests/src/unit/core/clipboard/paste/__snapshots__/text/plain/pasteHTMLWithParagraphsInTableCell.json new file mode 100644 index 0000000000..47d6a80276 --- /dev/null +++ b/tests/src/unit/core/clipboard/paste/__snapshots__/text/plain/pasteHTMLWithParagraphsInTableCell.json @@ -0,0 +1,59 @@ +[ + { + "children": [], + "content": { + "columnWidths": [ + undefined, + undefined, + ], + "headerCols": undefined, + "headerRows": undefined, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 1

Paragraph 1

Paragraph 2

Paragraph 3

", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + { + "content": [ + { + "styles": {}, + "text": "Cell 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "1", + "props": { + "textColor": "default", + }, + "type": "table", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/clipboard/paste/__snapshots__/text/plain/pasteMultilineTextInTableCell.json b/tests/src/unit/core/clipboard/paste/__snapshots__/text/plain/pasteMultilineTextInTableCell.json new file mode 100644 index 0000000000..43210208e8 --- /dev/null +++ b/tests/src/unit/core/clipboard/paste/__snapshots__/text/plain/pasteMultilineTextInTableCell.json @@ -0,0 +1,114 @@ +[ + { + "children": [], + "content": { + "columnWidths": [ + undefined, + ], + "headerCols": undefined, + "headerRows": undefined, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 1Line 1", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "1", + "props": { + "textColor": "default", + }, + "type": "table", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Line 2", + "type": "text", + }, + ], + "id": "2", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": [ + { + "styles": {}, + "text": "Line 3", + "type": "text", + }, + ], + "id": "3", + "props": { + "backgroundColor": "default", + "textAlignment": "left", + "textColor": "default", + }, + "type": "paragraph", + }, + { + "children": [], + "content": { + "columnWidths": [ + undefined, + ], + "headerCols": undefined, + "headerRows": undefined, + "rows": [ + { + "cells": [ + { + "content": [ + { + "styles": {}, + "text": "Cell 2", + "type": "text", + }, + ], + "props": { + "backgroundColor": "default", + "colspan": 1, + "rowspan": 1, + "textAlignment": "left", + "textColor": "default", + }, + "type": "tableCell", + }, + ], + }, + ], + "type": "tableContent", + }, + "id": "4", + "props": { + "textColor": "default", + }, + "type": "table", + }, +] \ No newline at end of file diff --git a/tests/src/unit/core/clipboard/paste/pasteTestInstances.ts b/tests/src/unit/core/clipboard/paste/pasteTestInstances.ts index 404cd8cb83..70682f6443 100644 --- a/tests/src/unit/core/clipboard/paste/pasteTestInstances.ts +++ b/tests/src/unit/core/clipboard/paste/pasteTestInstances.ts @@ -137,6 +137,56 @@ export const pasteTestInstancesHTML: TestInstance< }, executeTest: testPasteHTML, }, + { + testCase: { + name: "pasteMultilineTextInTableCell", + content: `Line 1\nLine 2\nLine 3`, + document: [ + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: [["Cell 1"], ["Cell 2"]], + }, + ], + }, + }, + ], + getPasteSelection: (doc) => { + const startPos = getPosOfTextNode(doc, "Cell 1", true); + + return TextSelection.create(doc, startPos); + }, + }, + executeTest: testPasteMarkdown, + }, + { + testCase: { + name: "pasteHTMLWithParagraphsInTableCell", + content: `

Paragraph 1

Paragraph 2

Paragraph 3

`, + document: [ + { + type: "table", + content: { + type: "tableContent", + rows: [ + { + cells: [["Cell 1"], ["Cell 2"]], + }, + ], + }, + }, + ], + getPasteSelection: (doc) => { + const startPos = getPosOfTextNode(doc, "Cell 1", true); + + return TextSelection.create(doc, startPos); + }, + }, + executeTest: testPasteHTML, + }, ]; export const pasteTestInstancesMarkdown: TestInstance<