Files
outline/shared/editor/nodes/TableCell.ts
2025-12-28 08:56:32 -05:00

164 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Token } from "markdown-it";
import {
type Node as ProsemirrorNode,
type NodeSpec,
Slice,
} from "prosemirror-model";
import type { EditorState } from "prosemirror-state";
import { Plugin, PluginKey } from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";
import { TableMap } from "prosemirror-tables";
import { getCellAttrs, setCellAttrs } from "../lib/table";
import Node from "./Node";
export default class TableCell extends Node {
get name() {
return "td";
}
get schema(): NodeSpec {
return {
content: "block+",
tableRole: "cell",
group: "cell",
isolating: true,
parseDOM: [{ tag: "td", getAttrs: getCellAttrs }],
toDOM(node) {
return ["td", setCellAttrs(node), 0];
},
attrs: {
colspan: { default: 1 },
rowspan: { default: 1 },
alignment: { default: null },
colwidth: { default: null },
},
};
}
toMarkdown() {
// see: renderTable
}
parseMarkdown() {
return {
block: "td",
getAttrs: (tok: Token) => ({ alignment: tok.info }),
};
}
get plugins() {
const createCellDecorations = (state: EditorState) => {
const { doc } = state;
const decorations: Decoration[] = [];
// Iterate through all tables in the document
doc.descendants((node: ProsemirrorNode, pos: number) => {
if (node.type.spec.tableRole === "table") {
const map = TableMap.get(node);
// Mark cells in the first column and last row of this table
node.descendants((cellNode: ProsemirrorNode, cellPos: number) => {
if (
cellNode.type.spec.tableRole === "cell" ||
cellNode.type.spec.tableRole === "header_cell"
) {
const cellOffset = cellPos;
const cellIndex = map.map.indexOf(cellOffset);
if (cellIndex !== -1) {
const col = cellIndex % map.width;
const row = Math.floor(cellIndex / map.width);
const rowspan = cellNode.attrs.rowspan || 1;
const colspan = cellNode.attrs.colspan || 1;
const attrs: Record<string, string> = {};
if (col === 0) {
attrs["data-first-column"] = "true";
}
// Mark cells that extend into the last column (accounting for colspan)
if (col + colspan >= map.width) {
attrs["data-last-column"] = "true";
}
// Mark cells that extend into the last row (accounting for rowspan)
if (row + rowspan >= map.height) {
attrs["data-last-row"] = "true";
}
if (Object.keys(attrs).length > 0) {
decorations.push(
Decoration.node(
pos + cellPos + 1,
pos + cellPos + 1 + cellNode.nodeSize,
attrs
)
);
}
}
}
});
}
});
return DecorationSet.create(doc, decorations);
};
return [
new Plugin({
key: new PluginKey("table-cell-attributes"),
state: {
init: (_, state) => createCellDecorations(state),
apply: (tr, pluginState, oldState, newState) => {
// Only recompute if document changed
if (!tr.docChanged) {
return pluginState;
}
return createCellDecorations(newState);
},
},
props: {
decorations(state) {
return this.getState(state);
},
},
}),
new Plugin({
key: new PluginKey("table-cell-copy-transform"),
props: {
transformCopied: (slice) => {
// check if the copied selection is a single table, with a single row, with a single cell. If so,
// copy the cell content only not a table with a single cell. This leads to more predictable pasting
// behavior, both in and outside the app.
if (slice.content.childCount === 1) {
const table = slice.content.firstChild;
if (
table?.type.spec.tableRole === "table" &&
table.childCount === 1
) {
const row = table.firstChild;
if (
row?.type.spec.tableRole === "row" &&
row.childCount === 1
) {
const cell = row.firstChild;
if (cell?.type.spec.tableRole === "cell") {
return new Slice(
cell.content,
slice.openStart,
slice.openEnd
);
}
}
}
}
return slice;
},
},
}),
];
}
}