Skip to content

Commit

Permalink
feat: support colspan and rowspan in table actions (#356)
Browse files Browse the repository at this point in the history
  • Loading branch information
d3m1d0v authored Sep 12, 2024
1 parent a3d7538 commit 65e4327
Show file tree
Hide file tree
Showing 7 changed files with 340 additions and 147 deletions.
17 changes: 1 addition & 16 deletions src/extensions/yfm/YfmTable/plugins/YfmTableControls/actions.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import {Node} from 'prosemirror-model';

import {CommandWithAttrs} from '../../../../../core';
import {
addColumnAfter,
addRowAfter,
appendColumn,
appendRow,
removeColumn,
removeRow,
} from '../../../../../table-utils';
import {appendColumn, appendRow, removeColumn, removeRow} from '../../../../../table-utils';
import {defineActions} from '../../../../../utils/actions';
import {removeNode} from '../../../../../utils/remove-node';

Expand Down Expand Up @@ -36,18 +29,10 @@ const removeYfmTable: CommandWithAttrs<{
return true;
};
export const controlActions = defineActions({
addRow: {
isEnable: addRowAfter,
run: addRowAfter,
},
deleteRow: {
isEnable: removeRow,
run: removeRow,
},
addColumn: {
isEnable: addColumnAfter,
run: addColumnAfter,
},
deleteColumn: {
isEnable: removeColumn,
run: removeColumn,
Expand Down
89 changes: 45 additions & 44 deletions src/table-utils/commands/appendColumn.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,70 @@
import {findChildren, findParentNodeClosestToPos} from 'prosemirror-utils';
import {findParentNodeClosestToPos} from 'prosemirror-utils';

import {isTableNode, isTableRowNode} from '..';
import {isTableNode} from '..';
import type {CommandWithAttrs} from '../../core';
import {findChildIndex} from '../helpers';
import {findChildTableCells, isTableBodyNode, isTableCellNode} from '../utils';
import {CellPos, TableDesc} from '../table-desc';

export const appendColumn: CommandWithAttrs<{
tablePos: number;
columnNumber?: number;
direction?: 'before' | 'after';
// eslint-disable-next-line complexity
}> = (state, dispatch, _, attrs) => {
if (!attrs) return false;
const {tablePos, columnNumber, direction} = attrs;
const parentTable = findParentNodeClosestToPos(state.doc.resolve(tablePos + 1), isTableNode)
?.node;

if (!parentTable) return false;
let parentCell;
let parentRow;
const {tablePos, columnNumber, direction = 'after'} = attrs;
const res = findParentNodeClosestToPos(state.doc.resolve(tablePos + 1), isTableNode);
if (!res) return false;

const tableBody = findChildren(parentTable, isTableBodyNode, false).pop();
if (!tableBody) return false;
const tableNode = res.node;
const tableDesc = TableDesc.create(tableNode);
if (!tableDesc) return false;

if (columnNumber !== undefined) {
parentCell = findChildTableCells(parentTable)[columnNumber];
parentRow = findParentNodeClosestToPos(
state.doc.resolve(tablePos + parentCell.pos + 1),
isTableRowNode,
);
} else {
parentRow = findChildren(tableBody.node, isTableRowNode, false).pop();
if (!parentRow) return false;
const columnIndex = columnNumber ?? tableDesc.cols - 1; // if columnNumber is not defined, that means last row
const isFirstColumn = columnIndex === 0;
const isLastColumn = columnIndex === tableDesc.cols - 1;

parentCell = findChildren(parentRow.node, isTableCellNode, false).pop();
}

if (!parentCell || !parentRow || !parentTable) {
return false;
}
let pos: number[] | null = null;
if (isFirstColumn && direction === 'before')
pos = tableDesc.getRelativePosForColumn(0).map(fromOrClosest);
if (isLastColumn && direction === 'after')
pos = tableDesc.getRelativePosForColumn(tableDesc.cols - 1).map(toOrClosest);

const parentCellIndex = columnNumber || findChildIndex(parentRow.node, parentCell.node);
if (!pos) {
if (tableDesc.cols <= columnIndex) return false;

if (parentCellIndex < 0) {
return false;
if (tableDesc.isSafeColumn(columnIndex)) {
const columnPos = tableDesc.getRelativePosForColumn(columnIndex);
if (direction === 'before') pos = columnPos.map(fromOrClosest);
if (direction === 'after') pos = columnPos.map(toOrClosest);
} else {
if (direction === 'before' && tableDesc.isSafeColumn(columnIndex - 1))
pos = tableDesc.getRelativePosForColumn(columnIndex - 1).map(toOrClosest);
if (direction === 'after' && tableDesc.isSafeColumn(columnIndex + 1))
pos = tableDesc.getRelativePosForColumn(columnIndex + 1).map(fromOrClosest);
}
}

if (dispatch) {
const allRows = findChildren(tableBody.node, isTableRowNode, false);
if (!pos) return false;

let tr = state.tr;
for (const row of allRows) {
const rowCells = findChildren(row.node, isTableCellNode, false);
const cell = rowCells[parentCellIndex];

let position = tablePos + row.pos + cell.pos + 3;
position += direction === 'before' ? 0 : cell.node.nodeSize;
if (dispatch) {
const cellType = tableDesc.getCellNodeType();
const {tr} = state;

tr = tr.insert(
tr.mapping.map(position),
cell.node.type.createAndFill(cell.node.attrs)!,
);
for (const p of pos) {
tr.insert(tr.mapping.map(res.pos + p), cellType.createAndFill()!);
}

dispatch(tr.scrollIntoView());
dispatch(tr);
}

return true;
};

function fromOrClosest(pos: CellPos): number {
return pos.type === 'real' ? pos.from : pos.closestPos;
}

function toOrClosest(pos: CellPos): number {
return pos.type === 'real' ? pos.to : pos.closestPos;
}
73 changes: 44 additions & 29 deletions src/table-utils/commands/appendRow.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,62 @@
import {Fragment, Node} from 'prosemirror-model';
import {findChildren, findParentNodeClosestToPos} from 'prosemirror-utils';
import {Node, NodeType} from 'prosemirror-model';
import {findParentNodeClosestToPos} from 'prosemirror-utils';

import {findChildTableRows, isTableBodyNode, isTableNode, isTableRowNode} from '..';
import {isTableNode} from '..';
import type {CommandWithAttrs} from '../../core';
import {TableDesc} from '../table-desc';

export const appendRow: CommandWithAttrs<{
tablePos: number;
rowNumber?: number;
direction?: 'before' | 'after';
// eslint-disable-next-line complexity
}> = (state, dispatch, _, attrs) => {
if (!attrs) return false;
const {tablePos, rowNumber, direction} = attrs;

const tableNode = findParentNodeClosestToPos(state.doc.resolve(tablePos + 1), isTableNode)
?.node;

if (!tableNode) return false;

const parentBody = findChildren(tableNode, isTableBodyNode, false).pop();
if (!parentBody) return false;

let parentRow;
if (rowNumber !== undefined) {
parentRow = findChildTableRows(tableNode)[rowNumber];
} else {
parentRow = findChildren(parentBody.node, isTableRowNode, false).pop();
const {tablePos, rowNumber, direction = 'after'} = attrs;
const res = findParentNodeClosestToPos(state.doc.resolve(tablePos + 1), isTableNode);
if (!res) return false;

const tableNode = res.node;
const tableDesc = TableDesc.create(tableNode);
if (!tableDesc) return false;

const rowIndex = rowNumber ?? tableDesc.rows - 1; // if rowNumber is not defined, that means last row
const isFirstRow = rowIndex === 0;
const isLastRow = rowIndex === tableDesc.rows - 1;

let pos = -1;
if (isFirstRow && direction === 'before') pos = tableDesc.getRelativePosForRow(0).from;
if (isLastRow && direction === 'after')
pos = tableDesc.getRelativePosForRow(tableDesc.rows - 1).to;

if (pos === -1) {
if (tableDesc.rows <= rowIndex) return false;

if (tableDesc.isSafeRow(rowIndex)) {
const rowPos = tableDesc.getRelativePosForRow(rowIndex);
if (direction === 'before') pos = rowPos.from;
if (direction === 'after') pos = rowPos.to;
} else {
if (direction === 'before' && tableDesc.isSafeRow(rowIndex - 1))
pos = tableDesc.getRelativePosForRow(rowIndex - 1).to;
if (direction === 'after' && tableDesc.isSafeRow(rowIndex + 1))
pos = tableDesc.getRelativePosForRow(rowIndex + 1).from;
}
}

if (!parentRow) {
return false;
}
if (pos === -1) return false;

if (dispatch) {
const newCellNodes: Node[] = [];
parentRow.node.forEach((node) => {
newCellNodes.push(node.type.createAndFill(node.attrs)!);
});

let position = tablePos + parentRow.pos;
position += direction === 'before' ? 1 : parentRow.node.nodeSize + 1;

dispatch(state.tr.insert(position, parentRow.node.copy(Fragment.from(newCellNodes))));
const cells = getNodes(tableDesc.getCellNodeType(), tableDesc.cols);
dispatch(state.tr.insert(res.pos + pos, tableDesc.getRowNodeType().create(null, cells)));
}

return true;
};

function getNodes(type: NodeType, count: number) {
const nodes: Node[] = [];
for (let i = 0; i < count; i++) nodes.push(type.createAndFill()!);
return nodes;
}
43 changes: 21 additions & 22 deletions src/table-utils/commands/removeColumn.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {Command} from 'prosemirror-state';

import {isTableNode} from '..';
import {isTableBodyNode, isTableCellNode, isTableRowNode} from '../utils';
import {TableDesc} from '../table-desc';
import {isTableNode} from '../utils';

export const removeColumn: Command = (
state,
Expand All @@ -18,30 +18,29 @@ export const removeColumn: Command = (
const tableNode = state.doc.nodeAt(tablePos);
if (!tableNode || tableNode.nodeSize <= 2 || !isTableNode(tableNode)) return false;

const tableBodyNode = tableNode.firstChild;
if (!tableBodyNode || tableBodyNode.nodeSize <= 2 || !isTableBodyNode(tableBodyNode))
return false;
const tableDesc = TableDesc.create(tableNode);
if (!tableDesc) return false;

// there is one column left
if (tableBodyNode.firstChild && tableBodyNode.firstChild.childCount < 2) return false;
if (
!tableDesc ||
// there is one column left
tableDesc.cols < 2 ||
tableDesc.cols <= columnNumber ||
!tableDesc.isSafeColumn(columnNumber)
)
return false;

if (dispatch) {
const {tr} = state;

tableBodyNode.forEach((rowNode, rowOffset) => {
if (!isTableRowNode(rowNode)) return;

rowNode.forEach((cellNode, cellOffset, cellIndex) => {
if (!isTableCellNode(cellNode)) return;

if (cellIndex === columnNumber) {
// table -> tbody -> tr -> td
const from = tablePos + 2 + rowOffset + 1 + cellOffset;
const to = from + cellNode.nodeSize;
tr.delete(tr.mapping.map(from), tr.mapping.map(to));
}
});
});
const pos = tableDesc.getRelativePosForColumn(columnNumber);
for (const item of pos) {
if (item.type === 'real') {
let {from, to} = item;
from += tablePos;
to += tablePos;
tr.delete(tr.mapping.map(from), tr.mapping.map(to));
}
}

dispatch(tr);
}
Expand Down
43 changes: 17 additions & 26 deletions src/table-utils/commands/removeRow.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {Fragment} from 'prosemirror-model';
import type {Command} from 'prosemirror-state';
import {findChildren, findParentNodeClosestToPos} from 'prosemirror-utils';
import {findParentNodeClosestToPos} from 'prosemirror-utils';

import {findChildTableBody, isTableNode, isTableRowNode} from '..';
import {isTableNode} from '..';
import {trackTransactionMetrics} from '../../core';
import {findChildTableRows} from '../utils';
import {TableDesc} from '../table-desc';

export const removeRow: Command = (
state,
Expand All @@ -20,31 +20,22 @@ export const removeRow: Command = (
?.node;

if (!tableNode) return false;
const parentRows = findChildren(tableNode, isTableRowNode);

const parentBody = findChildTableBody(tableNode)[0];
const parentRow = parentRows[rowNumber];

if (!parentRows.length || !parentBody) {
return false;
}

if (findChildTableRows(parentBody.node).length < 2) {
// there is one row left
return false;
const tableDesc = TableDesc.create(tableNode);
if (!tableDesc || rowNumber >= tableDesc.rows) return false;
if (!tableDesc.isSafeRow(rowNumber)) return false;

if (dispatch) {
let {from, to} = tableDesc.getRelativePosForRow(rowNumber);
from += tablePos;
to += tablePos;
dispatch(
trackTransactionMetrics(state.tr.replaceWith(from, to, Fragment.empty), 'removeRow', {
rows: tableDesc.rows,
cols: tableDesc.cols,
}),
);
}

dispatch?.(
trackTransactionMetrics(
state.tr.replaceWith(
tablePos + parentRow.pos,
tablePos + parentRow.pos + parentRow.node.nodeSize + 1,
Fragment.empty,
),
'removeRow',
{rows: parentRows.length, cols: parentRow.node.childCount},
),
);

return true;
};
Loading

0 comments on commit 65e4327

Please sign in to comment.