mirror of
https://github.com/outline/outline.git
synced 2026-01-06 02:59:54 -06:00
164 lines
4.9 KiB
TypeScript
164 lines
4.9 KiB
TypeScript
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;
|
||
},
|
||
},
|
||
}),
|
||
];
|
||
}
|
||
}
|