Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tom Moor <tom.moor@gmail.com>
202 lines
5.6 KiB
TypeScript
202 lines
5.6 KiB
TypeScript
import { NodeSpec, Node as ProsemirrorNode, Schema } from "prosemirror-model";
|
|
import { EditorState, Plugin, TextSelection } from "prosemirror-state";
|
|
import {
|
|
addColumnAfter,
|
|
addColumnBefore,
|
|
deleteColumn,
|
|
deleteRow,
|
|
deleteTable,
|
|
goToNextCell,
|
|
isInTable,
|
|
tableEditing,
|
|
toggleHeaderCell,
|
|
toggleHeaderColumn,
|
|
toggleHeaderRow,
|
|
CellSelection,
|
|
} from "prosemirror-tables";
|
|
import {
|
|
addRowAt,
|
|
createTable,
|
|
getCellsInColumn,
|
|
moveRow,
|
|
setTextSelection,
|
|
} from "prosemirror-utils";
|
|
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
|
import { getRowIndexFromText } from "../queries/getRowIndex";
|
|
import tablesRule from "../rules/tables";
|
|
import { Dispatch } from "../types";
|
|
import Node from "./Node";
|
|
|
|
export default class Table extends Node {
|
|
get name() {
|
|
return "table";
|
|
}
|
|
|
|
get schema(): NodeSpec {
|
|
return {
|
|
content: "tr+",
|
|
tableRole: "table",
|
|
isolating: true,
|
|
group: "block",
|
|
parseDOM: [{ tag: "table" }],
|
|
toDOM() {
|
|
return [
|
|
"div",
|
|
{ class: "scrollable-wrapper table-wrapper" },
|
|
[
|
|
"div",
|
|
{ class: "scrollable" },
|
|
["table", { class: "rme-table" }, ["tbody", 0]],
|
|
],
|
|
];
|
|
},
|
|
};
|
|
}
|
|
|
|
get rulePlugins() {
|
|
return [tablesRule];
|
|
}
|
|
|
|
commands({ schema }: { schema: Schema }) {
|
|
return {
|
|
createTable:
|
|
({ rowsCount, colsCount }: { rowsCount: number; colsCount: number }) =>
|
|
(state: EditorState, dispatch: Dispatch) => {
|
|
const offset = state.tr.selection.anchor + 1;
|
|
const nodes = createTable(schema, rowsCount, colsCount);
|
|
const tr = state.tr.replaceSelectionWith(nodes).scrollIntoView();
|
|
const resolvedPos = tr.doc.resolve(offset);
|
|
|
|
tr.setSelection(TextSelection.near(resolvedPos));
|
|
dispatch(tr);
|
|
return true;
|
|
},
|
|
setColumnAttr:
|
|
({ index, alignment }: { index: number; alignment: string }) =>
|
|
(state: EditorState, dispatch: Dispatch) => {
|
|
const cells = getCellsInColumn(index)(state.selection) || [];
|
|
let transaction = state.tr;
|
|
cells.forEach(({ pos }) => {
|
|
transaction = transaction.setNodeMarkup(pos, undefined, {
|
|
alignment,
|
|
});
|
|
});
|
|
dispatch(transaction);
|
|
return true;
|
|
},
|
|
addColumnBefore: () => addColumnBefore,
|
|
addColumnAfter: () => addColumnAfter,
|
|
deleteColumn: () => deleteColumn,
|
|
addRowAfter:
|
|
({ index }: { index: number }) =>
|
|
(state: EditorState, dispatch: Dispatch) => {
|
|
if (index === 0) {
|
|
// A little hack to avoid cloning the heading row by cloning the row
|
|
// beneath and then moving it to the right index.
|
|
const tr = addRowAt(index + 2, true)(state.tr);
|
|
dispatch(moveRow(index + 2, index + 1)(tr));
|
|
} else {
|
|
dispatch(addRowAt(index + 1, true)(state.tr));
|
|
}
|
|
return true;
|
|
},
|
|
deleteRow: () => deleteRow,
|
|
deleteTable: () => deleteTable,
|
|
toggleHeaderColumn: () => toggleHeaderColumn,
|
|
toggleHeaderRow: () => toggleHeaderRow,
|
|
toggleHeaderCell: () => toggleHeaderCell,
|
|
};
|
|
}
|
|
|
|
keys() {
|
|
return {
|
|
Tab: goToNextCell(1),
|
|
"Shift-Tab": goToNextCell(-1),
|
|
Enter: (state: EditorState, dispatch: Dispatch) => {
|
|
if (!isInTable(state)) {
|
|
return false;
|
|
}
|
|
const index = getRowIndexFromText(
|
|
state.selection as unknown as CellSelection
|
|
);
|
|
|
|
if (index === 0) {
|
|
const cells = getCellsInColumn(0)(state.selection);
|
|
if (!cells) {
|
|
return false;
|
|
}
|
|
|
|
const tr = addRowAt(index + 2, true)(state.tr);
|
|
dispatch(
|
|
setTextSelection(cells[1].pos)(moveRow(index + 2, index + 1)(tr))
|
|
);
|
|
} else {
|
|
dispatch(addRowAt(index + 1, true)(state.tr));
|
|
}
|
|
return true;
|
|
},
|
|
};
|
|
}
|
|
|
|
toMarkdown(state: MarkdownSerializerState, node: ProsemirrorNode) {
|
|
state.renderTable(node);
|
|
state.closeBlock(node);
|
|
}
|
|
|
|
parseMarkdown() {
|
|
return { block: "table" };
|
|
}
|
|
|
|
get plugins() {
|
|
return [
|
|
tableEditing(),
|
|
new Plugin({
|
|
props: {
|
|
decorations: (state) => {
|
|
const { doc } = state;
|
|
const decorations: Decoration[] = [];
|
|
let index = 0;
|
|
|
|
doc.descendants((node, pos) => {
|
|
if (node.type.name !== this.name) {
|
|
return;
|
|
}
|
|
|
|
const elements = document.getElementsByClassName("rme-table");
|
|
const table = elements[index];
|
|
if (!table) {
|
|
return;
|
|
}
|
|
|
|
const element = table.parentElement;
|
|
const shadowRight = !!(
|
|
element && element.scrollWidth > element.clientWidth
|
|
);
|
|
|
|
if (shadowRight) {
|
|
decorations.push(
|
|
Decoration.widget(
|
|
pos + 1,
|
|
() => {
|
|
const shadow = document.createElement("div");
|
|
shadow.className = "scrollable-shadow right";
|
|
return shadow;
|
|
},
|
|
{
|
|
key: "table-shadow-right",
|
|
}
|
|
)
|
|
);
|
|
}
|
|
index++;
|
|
});
|
|
|
|
return DecorationSet.create(doc, decorations);
|
|
},
|
|
},
|
|
}),
|
|
];
|
|
}
|
|
}
|