Skip to content
76 changes: 66 additions & 10 deletions packages/core/src/api/nodeConversions/nodeConversions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -187,6 +205,7 @@ export function tableContentToNodes<
const rowNode = schema.nodes["tableRow"].create({}, columnNodes);
rowNodes.push(rowNode);
}

return rowNodes;
}

Expand Down Expand Up @@ -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<string, string> = {
...(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<I, S>;

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);
Expand Down
86 changes: 82 additions & 4 deletions packages/core/src/blocks/TableBlockContent/TableBlockContent.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -44,6 +44,13 @@ const TableParagraph = Node.create({
group: "tableContent",
content: "inline*",

addAttributes() {
return {
width: {
default: "default",
},
};
},
parseHTML() {
return [
{ tag: "td" },
Expand Down Expand Up @@ -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(
Expand All @@ -85,6 +162,7 @@ export const Table = createBlockSpecFromStronglyTypedTiptapNode(
[
TableExtension,
TableParagraph,
TableImage,
TableHeader.extend({
content: "tableContent",
}),
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/blocks/TableBlockContent/TableExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
113 changes: 113 additions & 0 deletions packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -100,6 +101,7 @@ export class TableHandlesView<
> implements PluginView
{
public state?: TableHandlesState<I, S>;
public resizingTable?: HTMLElement;
public emitUpdate: () => void;

public tableId: string | undefined;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/i18n/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const fr: Dictionary = {
"média",
"url",
],
group: "Médias",
group: "Média",
},
video: {
title: "Vidéo",
Expand Down
Loading