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); }, }, }), ]; } }