diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index 9d76b4e84c..43b7cf123a 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -172,13 +172,31 @@ export function tableContentToNodes< const columnNodes: Node[] = []; for (const cell of row.cells) { let pNode: Node; - if (!cell) { + if (!cell || cell.length === 0) { pNode = schema.nodes["tableParagraph"].create({}); } else if (typeof cell === "string") { pNode = schema.nodes["tableParagraph"].create({}, schema.text(cell)); } else { - const textNodes = inlineContentToNodes(cell, schema, styleSchema); - pNode = schema.nodes["tableParagraph"].create({}, textNodes); + const isImage = cell.find( + (c: any) => c.type === "tableImage" + ) as unknown as { + url: string; + width: string; + styles: any; + }; + if (isImage) { + pNode = schema.nodes["tableImage"].create({ + src: isImage.url, + width: isImage.width, + backgroundColor: isImage.styles?.backgroundColor, + }); + } else { + const textNodes = inlineContentToNodes(cell, schema, styleSchema); + pNode = schema.nodes["tableParagraph"].create( + (cell[0] as any) ?? {}, + textNodes + ); + } } const cellNode = schema.nodes["tableCell"].create({}, pNode); @@ -187,6 +205,7 @@ export function tableContentToNodes< const rowNode = schema.nodes["tableRow"].create({}, columnNodes); rowNodes.push(rowNode); } + return rowNodes; } @@ -282,13 +301,50 @@ function contentNodeToTableContent< }; rowNode.content.forEach((cellNode) => { - row.cells.push( - contentNodeToInlineContent( - cellNode.firstChild!, - inlineContentSchema, - styleSchema - ) - ); + const firstChild = cellNode.firstChild; + + if (firstChild && firstChild.type.name === "tableImage") { + const styles: Record = { + ...(firstChild.attrs.backgroundColor && + firstChild.attrs.backgroundColor !== "default" + ? { backgroundColor: firstChild.attrs.backgroundColor } + : {}), + }; + const addStyles = Object.keys(styles).length > 0; + const imageCell = { + type: "tableImage", + url: firstChild.attrs.src, + ...(firstChild.attrs.width && firstChild.attrs.width !== "default" + ? { width: firstChild.attrs.width } + : {}), + ...(addStyles ? { styles } : {}), + } as unknown as InlineContent; + + row.cells.push([imageCell]); + return; + } + + const cells = contentNodeToInlineContent( + cellNode.firstChild!, + inlineContentSchema, + styleSchema + ).map((c) => ({ + ...c, + ...(firstChild!.attrs.width && firstChild!.attrs.width !== "default" + ? { width: firstChild!.attrs.width } + : {}), + })) as any; + if (cells.length === 0) { + cells.push({ + type: "text", + text: "", + styles: {}, + ...(firstChild!.attrs.width && firstChild!.attrs.width !== "default" + ? { width: firstChild!.attrs.width } + : {}), + }); + } + row.cells.push(cells); }); ret.rows.push(row); diff --git a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts index e47a62b2d4..d641f5251b 100644 --- a/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts +++ b/packages/core/src/blocks/TableBlockContent/TableBlockContent.ts @@ -1,4 +1,4 @@ -import { mergeAttributes, Node } from "@tiptap/core"; +import { Node } from "@tiptap/core"; import { TableCell } from "@tiptap/extension-table-cell"; import { TableHeader } from "@tiptap/extension-table-header"; import { TableRow } from "@tiptap/extension-table-row"; @@ -44,6 +44,13 @@ const TableParagraph = Node.create({ group: "tableContent", content: "inline*", + addAttributes() { + return { + width: { + default: "default", + }, + }; + }, parseHTML() { return [ { tag: "td" }, @@ -71,12 +78,82 @@ const TableParagraph = Node.create({ }, renderHTML({ HTMLAttributes }) { + const p = document.createElement("p"); + p.style.setProperty("min-width", "100px", "important"); + + if (HTMLAttributes.width && HTMLAttributes.width !== "default") { + p.style.width = HTMLAttributes.width; + } + return { + dom: p, + contentDOM: p, + }; + }, +}); + +const TableImage = Node.create({ + name: "tableImage", + group: "tableContent", + content: "inline*", + + addAttributes() { + return { + src: { + default: "", + }, + width: { + default: "default", + }, + backgroundColor: { + default: "default", + }, + }; + }, + parseHTML() { return [ - "p", - mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), - 0, + { tag: "td" }, + { + tag: "img", + getAttrs: (element) => { + if (typeof element === "string" || !element.textContent) { + return false; + } + + const parent = element.parentElement; + + if (parent === null) { + return false; + } + + if (parent.tagName === "TD") { + return {}; + } + + return false; + }, + }, ]; }, + + renderHTML({ HTMLAttributes }) { + const editor = this.options.editor; + const img = document.createElement("img"); + img.className = "table-image"; + editor.resolveFileUrl(HTMLAttributes.src).then((downloadUrl: string) => { + img.src = downloadUrl; + }); + + img.contentEditable = "false"; + img.draggable = false; + img.style.backgroundColor = HTMLAttributes.backgroundColor; + if (HTMLAttributes.width && HTMLAttributes.width !== "default") { + img.style.width = HTMLAttributes.width; + } + + return { + dom: img, + }; + }, }); export const Table = createBlockSpecFromStronglyTypedTiptapNode( @@ -85,6 +162,7 @@ export const Table = createBlockSpecFromStronglyTypedTiptapNode( [ TableExtension, TableParagraph, + TableImage, TableHeader.extend({ content: "tableContent", }), diff --git a/packages/core/src/blocks/TableBlockContent/TableExtension.ts b/packages/core/src/blocks/TableBlockContent/TableExtension.ts index fb0147d7ae..a36a5d89af 100644 --- a/packages/core/src/blocks/TableBlockContent/TableExtension.ts +++ b/packages/core/src/blocks/TableBlockContent/TableExtension.ts @@ -26,13 +26,22 @@ export const TableExtension = Extension.create({ return true; } - + if ( + this.editor.state.selection.empty && + this.editor.state.selection.$head.parent.type.name === "tableImage" + ) { + return true; + } return false; }, // Ensures that backspace won't delete the table if the text cursor is at // the start of a cell and the selection is empty. Backspace: () => { const selection = this.editor.state.selection; + + if (selection.$head.node().type.name === "tableImage") { + return false; + } const selectionIsEmpty = selection.empty; const selectionIsAtStartOfNode = selection.$head.parentOffset === 0; const selectionIsInTableParagraphNode = diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index a94b0b0e9a..49a0433e2e 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -12,6 +12,7 @@ import { } from "../../schema"; import { EventEmitter } from "../../util/EventEmitter"; import { getDraggableBlockFromElement } from "../SideMenu/SideMenuPlugin"; +import { CellSelection, cellAround } from "prosemirror-tables"; let dragImageElement: HTMLElement | undefined; @@ -100,6 +101,7 @@ export class TableHandlesView< > implements PluginView { public state?: TableHandlesState; + public resizingTable?: HTMLElement; public emitUpdate: () => void; public tableId: string | undefined; @@ -127,6 +129,9 @@ export class TableHandlesView< }; pmView.dom.addEventListener("mousemove", this.mouseMoveHandler); + pmView.dom.addEventListener("mouseup", this.mouseUpHandler); + pmView.dom.addEventListener("mousedown", this.mouseDownHandler); + pmView.dom.addEventListener("click", this.mouseClickHandler); pmView.root.addEventListener( "dragover", @@ -376,8 +381,116 @@ export class TableHandlesView< } }; + mouseDownHandler = (event: MouseEvent) => { + if (this.state === undefined) { + return; + } + + if (this.state.block.type !== "table") { + return; + } + + this.resizingTable = (event.target as any)?.closest("table") || undefined; + return; + }; + + mouseClickHandler = (event: MouseEvent) => { + if (this.state === undefined) { + return; + } + + if (this.state.block.type !== "table") { + return; + } + if ((event.target as any).className === "table-image") { + const image = this.editor._tiptapEditor.view.posAtCoords({ + left: event.clientX, + top: event.clientY, + })!; + const cell = cellAround( + this.editor._tiptapEditor.view.state.doc.resolve(image.pos) + )!; + + this.editor._tiptapEditor.view.dispatch( + this.editor._tiptapEditor.view.state.tr.setSelection( + new CellSelection(cell) + ) + ); + } + return; + }; + + mouseUpHandler = (event: MouseEvent) => { + if (this.state === undefined) { + return; + } + + event.preventDefault(); + if (this.state.block.type !== "table" || !this.resizingTable) { + return; + } + const rows = this.state.block.content.rows; + + const cols = this.resizingTable.querySelectorAll("col") ?? []; + const colWidth = Array.from(cols).map((col: any) => col.style.width); + let columnWidthChanged = false; + + const newRows = rows.map((row) => { + return { + cells: row.cells.map((cell, index) => { + if (cell.length === 0) { + if (!colWidth[index]) { + return []; + } + columnWidthChanged = true; + return [ + { + type: "text", + text: "", + width: colWidth[index], + styles: {}, + }, + ]; + } + return cell.map((c: any) => { + if (!colWidth[index]) { + return c; + } + if (c.width !== colWidth[index]) { + columnWidthChanged = true; + } + return { + ...c, + width: colWidth[index], + }; + }); + }), + }; + }); + + if (!columnWidthChanged) { + return; + } + const savedState = this.state; + setTimeout(() => { + savedState.block.content.rows = newRows; + + this.editor.updateBlock(savedState.block, { + type: "table", + content: { + type: "tableContent", + rows: newRows, + }, + }); + }, 0); + }; + destroy() { this.pmView.dom.removeEventListener("mousemove", this.mouseMoveHandler); + this.pmView.dom.removeEventListener("mousedown", this.mouseDownHandler); + this.pmView.dom.removeEventListener("mouseup", this.mouseUpHandler); + this.pmView.dom.addEventListener("click", this.mouseClickHandler); + this.pmView.root.removeEventListener( "dragover", this.dragOverHandler as EventListener diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts index bd9e073aa0..b27d6100c5 100644 --- a/packages/core/src/i18n/locales/fr.ts +++ b/packages/core/src/i18n/locales/fr.ts @@ -69,7 +69,7 @@ export const fr: Dictionary = { "média", "url", ], - group: "Médias", + group: "Média", }, video: { title: "Vidéo",