mirror of
https://github.com/outline/outline.git
synced 2025-12-29 23:09:55 -06:00
222 lines
6.7 KiB
TypeScript
222 lines
6.7 KiB
TypeScript
import { Token } from "markdown-it";
|
||
import { NodeSpec, Slice } from "prosemirror-model";
|
||
import { Plugin } from "prosemirror-state";
|
||
import { DecorationSet, Decoration } from "prosemirror-view";
|
||
import { addRowBefore, selectRow, selectTable } from "../commands/table";
|
||
import { getCellAttrs, setCellAttrs } from "../lib/table";
|
||
import {
|
||
getCellsInColumn,
|
||
getRowIndexInMap,
|
||
isRowSelected,
|
||
isTableSelected,
|
||
} from "../queries/table";
|
||
import { EditorStyleHelper } from "../styles/EditorStyleHelper";
|
||
import { cn } from "../styles/utils";
|
||
import Node from "./Node";
|
||
|
||
export default class TableCell extends Node {
|
||
get name() {
|
||
return "td";
|
||
}
|
||
|
||
get schema(): NodeSpec {
|
||
return {
|
||
content: "block+",
|
||
tableRole: "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() {
|
||
function buildAddRowDecoration(pos: number, index: number) {
|
||
const className = cn(EditorStyleHelper.tableAddRow, {
|
||
first: index === 0,
|
||
});
|
||
|
||
return Decoration.widget(
|
||
pos + 1,
|
||
() => {
|
||
const plus = document.createElement("a");
|
||
plus.role = "button";
|
||
plus.className = className;
|
||
plus.dataset.index = index.toString();
|
||
return plus;
|
||
},
|
||
{
|
||
key: cn(className, index),
|
||
}
|
||
);
|
||
}
|
||
|
||
return [
|
||
new Plugin({
|
||
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;
|
||
},
|
||
handleDOMEvents: {
|
||
mousedown: (view, event) => {
|
||
if (!(event.target instanceof HTMLElement)) {
|
||
return false;
|
||
}
|
||
|
||
const targetAddRow = event.target.closest(
|
||
`.${EditorStyleHelper.tableAddRow}`
|
||
);
|
||
if (targetAddRow) {
|
||
event.preventDefault();
|
||
event.stopImmediatePropagation();
|
||
const index = Number(targetAddRow.getAttribute("data-index"));
|
||
|
||
addRowBefore({ index })(view.state, view.dispatch);
|
||
return true;
|
||
}
|
||
|
||
const targetGrip = event.target.closest(
|
||
`.${EditorStyleHelper.tableGrip}`
|
||
);
|
||
if (targetGrip) {
|
||
event.preventDefault();
|
||
event.stopImmediatePropagation();
|
||
selectTable()(view.state, view.dispatch);
|
||
return true;
|
||
}
|
||
|
||
const targetGripRow = event.target.closest(
|
||
`.${EditorStyleHelper.tableGripRow}`
|
||
);
|
||
if (targetGripRow) {
|
||
event.preventDefault();
|
||
event.stopImmediatePropagation();
|
||
|
||
selectRow(
|
||
Number(targetGripRow.getAttribute("data-index")),
|
||
event.metaKey || event.shiftKey
|
||
)(view.state, view.dispatch);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
},
|
||
},
|
||
decorations: (state) => {
|
||
if (!this.editor.view?.editable) {
|
||
return;
|
||
}
|
||
|
||
const { doc } = state;
|
||
const decorations: Decoration[] = [];
|
||
const rows = getCellsInColumn(0)(state);
|
||
|
||
if (rows) {
|
||
rows.forEach((pos, visualIndex) => {
|
||
const actualRowIndex = getRowIndexInMap(visualIndex, state);
|
||
const index =
|
||
actualRowIndex !== -1 ? actualRowIndex : visualIndex;
|
||
if (index === 0) {
|
||
const className = cn(EditorStyleHelper.tableGrip, {
|
||
selected: isTableSelected(state),
|
||
});
|
||
|
||
decorations.push(
|
||
Decoration.widget(
|
||
pos + 1,
|
||
() => {
|
||
const grip = document.createElement("a");
|
||
grip.role = "button";
|
||
grip.className = className;
|
||
return grip;
|
||
},
|
||
{
|
||
key: className,
|
||
}
|
||
)
|
||
);
|
||
}
|
||
|
||
const className = cn(EditorStyleHelper.tableGripRow, {
|
||
selected:
|
||
isRowSelected(index)(state) || isTableSelected(state),
|
||
first: index === 0,
|
||
last: visualIndex === rows.length - 1,
|
||
});
|
||
|
||
decorations.push(
|
||
Decoration.widget(
|
||
pos + 1,
|
||
() => {
|
||
const grip = document.createElement("a");
|
||
grip.role = "button";
|
||
grip.className = className;
|
||
grip.dataset.index = index.toString();
|
||
return grip;
|
||
},
|
||
{
|
||
key: cn(className, index),
|
||
}
|
||
)
|
||
);
|
||
|
||
if (index === 0) {
|
||
decorations.push(buildAddRowDecoration(pos, index));
|
||
}
|
||
|
||
decorations.push(buildAddRowDecoration(pos, index + 1));
|
||
});
|
||
}
|
||
|
||
return DecorationSet.create(doc, decorations);
|
||
},
|
||
},
|
||
}),
|
||
];
|
||
}
|
||
}
|