diff --git a/packages/table/src/components/TableCellElement/useTableCellElementState.ts b/packages/table/src/components/TableCellElement/useTableCellElementState.ts index cd7a24c125..3dff3c1433 100644 --- a/packages/table/src/components/TableCellElement/useTableCellElementState.ts +++ b/packages/table/src/components/TableCellElement/useTableCellElementState.ts @@ -7,7 +7,7 @@ import { import { useReadOnly } from 'slate-react'; import { ELEMENT_TABLE, ELEMENT_TR } from '../../createTablePlugin'; -import { computeCellIndices } from '../../queries/computeCellIndices'; +import { computeCellIndices } from '../../merge/computeCellIndices'; import { getColSpan } from '../../queries/getColSpan'; import { getRowSpan } from '../../queries/getRowSpan'; import { getTableColumnIndex, getTableRowIndex } from '../../queries/index'; diff --git a/packages/table/src/components/TableElement/useTableElement.ts b/packages/table/src/components/TableElement/useTableElement.ts index 0a84039921..80e239a325 100644 --- a/packages/table/src/components/TableElement/useTableElement.ts +++ b/packages/table/src/components/TableElement/useTableElement.ts @@ -7,7 +7,7 @@ import { } from '@udecode/plate-common'; import { ELEMENT_TABLE } from '../../createTablePlugin'; -import { computeAllCellIndices } from '../../queries/computeCellIndices'; +import { computeAllCellIndices } from '../../merge/computeCellIndices'; import { useTableStore } from '../../stores/tableStore'; import { TablePlugin, TTableElement } from '../../types'; import { useSelectedCells } from './useSelectedCells'; diff --git a/packages/table/src/createTablePlugin.ts b/packages/table/src/createTablePlugin.ts index 7ecb072bb6..0c9b45c77a 100644 --- a/packages/table/src/createTablePlugin.ts +++ b/packages/table/src/createTablePlugin.ts @@ -36,6 +36,7 @@ export const createTablePlugin = createPluginFactory({ }); }, minColumnWidth: 48, + disableCellsMerging: false, _cellIndices: new WeakMap() as TableStoreCellAttributes, }, withOverrides: withTable, diff --git a/packages/table/src/queries/computeCellIndices.ts b/packages/table/src/merge/computeCellIndices.ts similarity index 93% rename from packages/table/src/queries/computeCellIndices.ts rename to packages/table/src/merge/computeCellIndices.ts index 009912c2d5..4a7e396b2c 100644 --- a/packages/table/src/queries/computeCellIndices.ts +++ b/packages/table/src/merge/computeCellIndices.ts @@ -62,16 +62,11 @@ export function computeCellIndices( }); if (rowIndex === -1 || colIndex === -1) { - // console.log('Invalid cell location.'); + console.log('Invalid cell location.'); return null; } const indices = { row: rowIndex, col: colIndex }; - const cellContent = cellEl.children?.map((i) => { - return (i.children as any)[0].text; - }); - // console.log('new cell location', cellContent, indices); - options?._cellIndices?.set(cellEl, indices); return indices; diff --git a/packages/table/src/merge/deleteColumn.ts b/packages/table/src/merge/deleteColumn.ts new file mode 100644 index 0000000000..699db5094b --- /dev/null +++ b/packages/table/src/merge/deleteColumn.ts @@ -0,0 +1,230 @@ +import { + getAboveNode, + getPluginOptions, + getPluginType, + insertElements, + PlateEditor, + removeNodes, + setNodes, + someNode, + Value, + withoutNormalizing, +} from '@udecode/plate-common'; +import { Path } from 'slate'; + +import { ELEMENT_TABLE, ELEMENT_TR } from '../createTablePlugin'; +import { getTableColumnCount } from '../queries'; +import { getColSpan } from '../queries/getColSpan'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getCellTypes } from '../utils'; +import { findCellByIndexes } from './findCellByIndexes'; +import { getCellPath } from './getCellPath'; +import { getIndices } from './getIndices'; + +export const deleteColumn = (editor: PlateEditor) => { + if ( + someNode(editor, { + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + }) + ) { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const tableEntry = getAboveNode(editor, { + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + }); + if (!tableEntry) return; + const table = tableEntry[0] as TTableElement; + + const selectedCellEntry = getAboveNode(editor, { + match: { + type: getCellTypes(editor), + }, + }); + if (!selectedCellEntry) return; + const selectedCell = selectedCellEntry[0] as TTableCellElement; + + const { col: deletingColIndex } = getIndices(options, selectedCell)!; + const colsDeleteNumber = getColSpan(selectedCell); + + const endingColIndex = deletingColIndex + colsDeleteNumber - 1; + + const rowNumber = table.children.length; + const affectedCellsSet = new Set(); + // iterating by rows is important here to keep the order of affected cells + Array.from({ length: rowNumber }, (_, i) => i).forEach((rI) => { + return Array.from({ length: colsDeleteNumber }, (_, i) => i).forEach( + (cI) => { + const colIndex = deletingColIndex + cI; + const found = findCellByIndexes(editor, table, rI, colIndex); + if (found) { + affectedCellsSet.add(found); + } + } + ); + }); + const affectedCells = Array.from(affectedCellsSet) as TTableCellElement[]; + + const { moveToNextColCells, squizeColSpanCells } = affectedCells.reduce<{ + squizeColSpanCells: TTableCellElement[]; + moveToNextColCells: TTableCellElement[]; + }>( + (acc, cur) => { + if (!cur) return acc; + + const currentCell = cur as TTableCellElement; + const { col: curColIndex } = getIndices(options, currentCell)!; + const curColSpan = getColSpan(currentCell); + + if (curColIndex < deletingColIndex && curColSpan > 1) { + acc.squizeColSpanCells.push(currentCell); + } else if ( + curColSpan > 1 && + curColIndex + curColSpan - 1 > endingColIndex + ) { + acc.moveToNextColCells.push(currentCell); + } + return acc; + }, + { moveToNextColCells: [], squizeColSpanCells: [] } + ); + + const nextColIndex = deletingColIndex + colsDeleteNumber; + const colNumber = getTableColumnCount(table); + if (colNumber > nextColIndex) { + moveToNextColCells.forEach((cur) => { + const curCell = cur as TTableCellElement; + const { col: curColIndex, row: curRowIndex } = getIndices( + options, + curCell + )!; + + const curColSpan = getColSpan(curCell); + + // simplify logic here. use getParent + const curRow = table.children[curRowIndex] as TTableRowElement; + const startingCellIndex = curRow.children.findIndex((curC) => { + const cell = curC as TTableCellElement; + const { col: cellColIndex } = getIndices(options, cell)!; + return cellColIndex >= curColIndex + 1; + }); + + const startingCell = curRow.children.at( + startingCellIndex + ) as TTableCellElement; + const { col: startingColIndex, row: startingRowIndex } = getIndices( + options, + startingCell + )!; + + const startingCellPath = getCellPath( + editor, + tableEntry, + startingRowIndex, + startingColIndex + ); + const colsNumberAffected = endingColIndex - curColIndex + 1; + + const newCell = { + ...curCell, + colSpan: curColSpan - colsNumberAffected, + }; + insertElements(editor, newCell, { at: startingCellPath }); + }); + } + + squizeColSpanCells.forEach((cur) => { + const curCell = cur as TTableCellElement; + + const { col: curColIndex, row: curColRowIndex } = getIndices( + options, + curCell + )!; + const curColSpan = getColSpan(curCell); + + const curCellPath = getCellPath( + editor, + tableEntry, + curColRowIndex, + curColIndex + ); + + const curCellEndingColIndex = Math.min( + curColIndex + curColSpan - 1, + endingColIndex + ); + const colsNumberAffected = curCellEndingColIndex - deletingColIndex + 1; + + setNodes( + editor, + { ...curCell, colSpan: curColSpan - colsNumberAffected }, + { at: curCellPath } + ); + }); + + const trEntry = getAboveNode(editor, { + match: { type: getPluginType(editor, ELEMENT_TR) }, + }); + + if ( + selectedCell && + trEntry && + tableEntry && + // Cannot delete the last cell + trEntry[0].children.length > 1 + ) { + const [tableNode, tablePath] = tableEntry; + + // calc paths to delete + const paths: Array = []; + affectedCells.forEach((cur) => { + const curCell = cur as TTableCellElement; + const { col: curColIndex, row: curRowIndex } = getIndices( + options, + curCell + )!; + + if (curColIndex >= deletingColIndex && curColIndex <= endingColIndex) { + const cellPath = getCellPath( + editor, + tableEntry, + curRowIndex, + curColIndex + ); + + if (!paths[curRowIndex]) { + paths[curRowIndex] = []; + } + paths[curRowIndex].push(cellPath); + } + }); + + withoutNormalizing(editor, () => { + paths.forEach((cellPaths) => { + const pathToDelete = cellPaths[0]; + cellPaths.forEach(() => { + removeNodes(editor, { + at: pathToDelete, + }); + }); + }); + + const { colSizes } = tableNode; + if (colSizes) { + const newColSizes = [...colSizes]; + newColSizes.splice(deletingColIndex, 1); + + setNodes( + editor, + { colSizes: newColSizes }, + { at: tablePath } + ); + } + }); + } + } +}; diff --git a/packages/table/src/merge/deleteRow.ts b/packages/table/src/merge/deleteRow.ts new file mode 100644 index 0000000000..9896298195 --- /dev/null +++ b/packages/table/src/merge/deleteRow.ts @@ -0,0 +1,172 @@ +import { + findNodePath, + getAboveNode, + getPluginOptions, + getPluginType, + insertElements, + PlateEditor, + removeNodes, + setNodes, + someNode, + Value, +} from '@udecode/plate-common'; + +import { ELEMENT_TABLE } from '../createTablePlugin'; +import { getTableColumnCount } from '../queries'; +import { getRowSpan } from '../queries/getRowSpan'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getCellTypes } from '../utils'; +import { findCellByIndexes } from './findCellByIndexes'; +import { getIndices } from './getIndices'; + +export const deleteRow = (editor: PlateEditor) => { + if ( + someNode(editor, { + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + }) + ) { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const currentTableItem = getAboveNode(editor, { + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + }); + if (!currentTableItem) return; + const table = currentTableItem[0] as TTableElement; + + const selectedCellEntry = getAboveNode(editor, { + match: { type: getCellTypes(editor) }, + }); + if (!selectedCellEntry) return; + + const selectedCell = selectedCellEntry[0] as TTableCellElement; + const { row: deletingRowIndex } = getIndices(options, selectedCell)!; + const rowsDeleteNumber = getRowSpan(selectedCell); + const endingRowIndex = deletingRowIndex + rowsDeleteNumber - 1; + + const colNumber = getTableColumnCount(table); + const affectedCellsSet = new Set(); + // iterating by columns is important here to keep the order of affected cells + Array.from({ length: colNumber }, (_, i) => i).forEach((cI) => { + return Array.from({ length: rowsDeleteNumber }, (_, i) => i).forEach( + (rI) => { + const rowIndex = deletingRowIndex + rI; + const found = findCellByIndexes(editor, table, rowIndex, cI); + affectedCellsSet.add(found); + } + ); + }); + const affectedCells = Array.from(affectedCellsSet) as TTableCellElement[]; + + const { moveToNextRowCells, squizeRowSpanCells } = affectedCells.reduce<{ + squizeRowSpanCells: TTableCellElement[]; + moveToNextRowCells: TTableCellElement[]; + }>( + (acc, cur) => { + if (!cur) return acc; + + const currentCell = cur as TTableCellElement; + const { row: curRowIndex } = getIndices(options, currentCell)!; + const curRowSpan = getRowSpan(currentCell); + + if (!curRowIndex || !curRowSpan) return acc; + + if (curRowIndex < deletingRowIndex && curRowSpan > 1) { + acc.squizeRowSpanCells.push(currentCell); + } else if ( + curRowSpan > 1 && + curRowIndex + curRowSpan - 1 > endingRowIndex + ) { + acc.moveToNextRowCells.push(currentCell); + } + + return acc; + }, + { squizeRowSpanCells: [], moveToNextRowCells: [] } + ); + + const nextRowIndex = deletingRowIndex + rowsDeleteNumber; + const nextRow = table.children[nextRowIndex] as + | TTableCellElement + | undefined; + + if (nextRow) { + moveToNextRowCells.forEach((cur, index) => { + const curRowCell = cur as TTableCellElement; + const { col: curRowCellColIndex } = getIndices(options, curRowCell)!; + const curRowCellRowSpan = getRowSpan(curRowCell); + + // search for anchor cell where to place current cell + const startingCellIndex = nextRow.children.findIndex((curC) => { + const cell = curC as TTableCellElement; + const { col: curColIndex } = getIndices(options, cell)!; + return curColIndex >= curRowCellColIndex; + }); + + const startingCell = nextRow.children[ + startingCellIndex + ] as TTableCellElement; + const { col: startingColIndex } = getIndices(options, startingCell)!; + + // consider already inserted cell by adding index each time to the col path + let incrementBy = index; + if (startingColIndex < curRowCellColIndex) { + // place current cell after starting cell, if placing cell col index is grather than col index of starting cell + incrementBy += 1; + } + + const startingCellPath = findNodePath(editor, startingCell)!; + const tablePath = startingCellPath.slice(0, -2); + const colPath = startingCellPath.at(-1)!; + + const nextRowStartCellPath = [ + ...tablePath, + nextRowIndex, + colPath + incrementBy, + ]; + + const rowsNumberAffected = endingRowIndex - curRowCellColIndex + 1; + + // TODO: consider make deep clone here + // making cell smaller and moving it to next row + const newCell = { + ...curRowCell, + rowSpan: curRowCellRowSpan - rowsNumberAffected, + }; + insertElements(editor, newCell, { at: nextRowStartCellPath }); + }); + } + + squizeRowSpanCells.forEach((cur) => { + const curRowCell = cur as TTableCellElement; + const { row: curRowCellRowIndex } = getIndices(options, curRowCell)!; + const curRowCellRowSpan = getRowSpan(curRowCell); + + const curCellPath = findNodePath(editor, curRowCell)!; + + const curCellEndingRowIndex = Math.min( + curRowCellRowIndex + curRowCellRowSpan - 1, + endingRowIndex + ); + const rowsNumberAffected = curCellEndingRowIndex - deletingRowIndex + 1; + + setNodes( + editor, + { ...curRowCell, rowSpan: curRowCellRowSpan - rowsNumberAffected }, + { at: curCellPath } + ); + }); + + const rowToDelete = table.children[deletingRowIndex] as TTableRowElement; + const rowPath = findNodePath(editor, rowToDelete); + Array.from({ length: rowsDeleteNumber }).forEach(() => { + removeNodes(editor, { + at: rowPath, + }); + }); + } +}; diff --git a/packages/table/src/queries/findCellByIndexes.ts b/packages/table/src/merge/findCellByIndexes.ts similarity index 92% rename from packages/table/src/queries/findCellByIndexes.ts rename to packages/table/src/merge/findCellByIndexes.ts index 09ba7f1aef..9607f688ca 100644 --- a/packages/table/src/queries/findCellByIndexes.ts +++ b/packages/table/src/merge/findCellByIndexes.ts @@ -1,10 +1,10 @@ import { getPluginOptions, PlateEditor, Value } from '@udecode/plate-common'; import { ELEMENT_TABLE } from '../createTablePlugin'; +import { getIndices } from '../merge/getIndices'; +import { getIndicesWithSpans } from '../merge/getIndicesWithSpans'; import { TablePlugin, TTableCellElement, TTableElement } from '../types'; import { computeCellIndices } from './computeCellIndices'; -import { getIndices } from './getIndices'; -import { getIndicesWithSpans } from './getIndicesWithSpans'; export const findCellByIndexes = ( editor: PlateEditor, diff --git a/packages/table/src/merge/getCellPath.ts b/packages/table/src/merge/getCellPath.ts new file mode 100644 index 0000000000..1195e9251d --- /dev/null +++ b/packages/table/src/merge/getCellPath.ts @@ -0,0 +1,33 @@ +import { + getPluginOptions, + PlateEditor, + TNodeEntry, + Value, +} from '@udecode/plate-common'; + +import { ELEMENT_TABLE } from '../createTablePlugin'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getIndices } from './getIndices'; + +export const getCellPath = ( + editor: PlateEditor, + tableEntry: TNodeEntry, + curRowIndex: number, + curColIndex: number +) => { + const options = getPluginOptions(editor, ELEMENT_TABLE); + const [tableNode, tablePath] = tableEntry; + + const rowElem = tableNode.children[curRowIndex] as TTableRowElement; + const foundColIndex = rowElem.children.findIndex((c) => { + const cE = c as TTableCellElement; + const { col: colIndex } = getIndices(options, cE)!; + return colIndex === curColIndex; + }); + return tablePath.concat([curRowIndex, foundColIndex]); +}; diff --git a/packages/table/src/queries/getIndices.ts b/packages/table/src/merge/getIndices.ts similarity index 100% rename from packages/table/src/queries/getIndices.ts rename to packages/table/src/merge/getIndices.ts diff --git a/packages/table/src/queries/getIndicesWithSpans.ts b/packages/table/src/merge/getIndicesWithSpans.ts similarity index 53% rename from packages/table/src/queries/getIndicesWithSpans.ts rename to packages/table/src/merge/getIndicesWithSpans.ts index ff69016c85..e4e908a634 100644 --- a/packages/table/src/queries/getIndicesWithSpans.ts +++ b/packages/table/src/merge/getIndicesWithSpans.ts @@ -1,8 +1,6 @@ -import { Value } from '@udecode/plate-common'; - -import { TablePlugin, TTableCellElement } from '../types'; -import { getColSpan } from './getColSpan'; -import { getRowSpan } from './getRowSpan'; +import { getColSpan } from '../queries/getColSpan'; +import { getRowSpan } from '../queries/getRowSpan'; +import { TTableCellElement } from '../types'; export const getIndicesWithSpans = ( { col, row }: { col: number; row: number }, diff --git a/packages/table/src/merge/getTableGridByRange.ts b/packages/table/src/merge/getTableGridByRange.ts new file mode 100644 index 0000000000..fe18fcf03b --- /dev/null +++ b/packages/table/src/merge/getTableGridByRange.ts @@ -0,0 +1,154 @@ +import { + findNode, + findNodePath, + getPluginOptions, + getPluginType, + PlateEditor, + TElement, + TElementEntry, + Value, +} from '@udecode/plate-common'; +import { Range } from 'slate'; + +import { ELEMENT_TABLE } from '../createTablePlugin'; +import { computeCellIndices } from '../merge/computeCellIndices'; +import { findCellByIndexes } from '../merge/findCellByIndexes'; +import { getIndices } from '../merge/getIndices'; +import { getIndicesWithSpans } from '../merge/getIndicesWithSpans'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getCellTypes } from '../utils'; +import { getEmptyTableNode } from '../utils/getEmptyTableNode'; + +export type FormatType = 'table' | 'cell' | 'all'; + +export interface TableGridEntries { + tableEntries: TElementEntry[]; + cellEntries: TElementEntry[]; +} + +export type GetTableGridReturnType = T extends 'all' + ? TableGridEntries + : TElementEntry[]; + +export interface GetTableGridByRangeOptions { + at: Range; + + /** + * Format of the output: + * - table element + * - array of cells + */ + format?: 'table' | 'cell'; +} + +/** + * Get sub table between 2 cell paths. + */ +export const getTableGridByRange = ( + editor: PlateEditor, + { at, format = 'table' }: GetTableGridByRangeOptions +): TElementEntry[] => { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const startCellEntry = findNode(editor, { + at: (at as any).anchor.path, + match: { type: getCellTypes(editor) }, + })!; // TODO: improve typing + const endCellEntry = findNode(editor, { + at: (at as any).focus.path, + match: { type: getCellTypes(editor) }, + })!; + + const startCell = startCellEntry[0] as TTableCellElement; + const endCell = endCellEntry[0] as TTableCellElement; + + const startCellPath = (at as any).anchor.path; + const tablePath = startCellPath.slice(0, -2); + + const tableEntry = findNode(editor, { + at: tablePath, + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + })!; // TODO: improve typing + const realTable = tableEntry[0] as TTableElement; + + const { col: _startColIndex, row: _startRowIndex } = + getIndices(options, startCell) || + computeCellIndices(editor, realTable, startCell)!; + + const { row: _endRowIndex, col: _endColIndex } = getIndicesWithSpans( + getIndices(options, endCell) || + computeCellIndices(editor, realTable, endCell)!, + endCell + ); + + const startRowIndex = Math.min(_startRowIndex, _endRowIndex); + const endRowIndex = Math.max(_startRowIndex, _endRowIndex); + const startColIndex = Math.min(_startColIndex, _endColIndex); + const endColIndex = Math.max(_startColIndex, _endColIndex); + + const relativeRowIndex = endRowIndex - startRowIndex; + const relativeColIndex = endColIndex - startColIndex; + + const table: TTableElement = getEmptyTableNode(editor, { + rowCount: relativeRowIndex + 1, + colCount: relativeColIndex + 1, + newCellChildren: [], + }); + + const cellEntries: TElementEntry[] = []; + const cellsSet = new WeakSet(); + + let rowIndex = startRowIndex; + let colIndex = startColIndex; + while (true) { + const cell = findCellByIndexes(editor, realTable, rowIndex, colIndex); + if (!cell) { + break; + } + + if (!cellsSet.has(cell)) { + cellsSet.add(cell); + + const rows = table.children[rowIndex - startRowIndex] + .children as TElement[]; + rows[colIndex - startColIndex] = cell; + + const cellPath = findNodePath(editor, cell)!; + cellEntries.push([cell, cellPath]); + } + + if (colIndex + 1 <= endColIndex) { + colIndex = colIndex + 1; + } else if (rowIndex + 1 <= endRowIndex) { + colIndex = startColIndex; + rowIndex = rowIndex + 1; + } else { + break; + } + } + + const formatType = (format as string) || 'table'; + + if (formatType === 'cell') { + return cellEntries; + } + + // clear redundant cells + table.children?.forEach((rowEl) => { + const rowElement = rowEl as TTableRowElement; + + const filteredChildren = rowElement.children?.filter((cellEl) => { + const cellElement = cellEl as TTableCellElement; + return !!cellElement?.children.length; + }); + + rowElement.children = filteredChildren; + }); + + return [[table, tablePath]]; +}; diff --git a/packages/table/src/merge/insertTableColumn.ts b/packages/table/src/merge/insertTableColumn.ts new file mode 100644 index 0000000000..e244ad77dc --- /dev/null +++ b/packages/table/src/merge/insertTableColumn.ts @@ -0,0 +1,221 @@ +import { + findNode, + getBlockAbove, + getNodeEntry, + getParentNode, + getPluginOptions, + getPluginType, + insertElements, + PlateEditor, + setNodes, + TDescendant, + TElement, + Value, + withoutNormalizing, +} from '@udecode/plate-common'; +import { Path } from 'slate'; + +import { ELEMENT_TABLE, ELEMENT_TH } from '../createTablePlugin'; +import { getColSpan } from '../queries/getColSpan'; +import { getRowSpan } from '../queries/getRowSpan'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getCellTypes, getEmptyCellNode } from '../utils'; +import { findCellByIndexes } from './findCellByIndexes'; +import { getCellPath } from './getCellPath'; +import { getIndices } from './getIndices'; + +const createEmptyCell = ( + editor: PlateEditor, + row: TTableRowElement, + newCellChildren?: TDescendant[], + header?: boolean +) => { + const isHeaderRow = + header === undefined + ? (row as TElement).children.every( + (c) => c.type === getPluginType(editor, ELEMENT_TH) + ) + : header; + + return getEmptyCellNode(editor, { + header: isHeaderRow, + newCellChildren, + }); +}; + +export const insertTableColumn = ( + editor: PlateEditor, + { + disableSelect, + fromCell, + at, + header, + }: { + header?: boolean; + + /** + * Path of the cell to insert the column from. + */ + fromCell?: Path; + + /** + * Exact path of the cell to insert the column at. + * Will overrule `fromCell`. + */ + at?: Path; + + /** + * Disable selection after insertion. + */ + disableSelect?: boolean; + } = {} +) => { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const cellEntry = fromCell + ? findNode(editor, { + at: fromCell, + match: { type: getCellTypes(editor) }, + }) + : getBlockAbove(editor, { + match: { type: getCellTypes(editor) }, + }); + if (!cellEntry) return; + + const [, cellPath] = cellEntry; + const cell = cellEntry[0] as TTableCellElement; + + const tableEntry = getBlockAbove(editor, { + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + at: cellPath, + }); + if (!tableEntry) return; + + const { newCellChildren, initialTableWidth, minColumnWidth } = + getPluginOptions(editor, ELEMENT_TABLE); + const [tableNode, tablePath] = tableEntry; + + const { col: cellColIndex } = getIndices(options, cell)!; + const cellColSpan = getColSpan(cell); + + let nextColIndex: number; + let checkingColIndex: number; + if (Path.isPath(at)) { + nextColIndex = cellColIndex; + checkingColIndex = cellColIndex - 1; + } else { + nextColIndex = cellColIndex + cellColSpan; + checkingColIndex = cellColIndex + cellColSpan - 1; + } + + const currentRowIndex = cellPath.at(-2); // recheck it + const rowNumber = tableNode.children.length; + const firstCol = nextColIndex <= 0; + + // const colCount = getTableColumnCount(tableNode); + // const lastRow = nextColIndex === colCount; + + let placementCorrection = 1; + if (firstCol) { + checkingColIndex = 0; + placementCorrection = 0; + } + + const affectedCellsSet = new Set(); + Array.from({ length: rowNumber }, (_, i) => i).forEach((rI) => { + const found = findCellByIndexes(editor, tableNode, rI, checkingColIndex); + if (found) { + affectedCellsSet.add(found); + } + }); + const affectedCells = Array.from(affectedCellsSet) as TTableCellElement[]; + + affectedCells.forEach((cur) => { + const curCell = cur as TTableCellElement; + const { row: curRowIndex, col: curColIndex } = getIndices( + options, + curCell + )!; + const curRowSpan = getRowSpan(curCell); + const curColSpan = getColSpan(curCell); + + const currentCellPath = getCellPath( + editor, + tableEntry, + curRowIndex, + curColIndex + ); + + const endCurI = curColIndex + curColSpan - 1; + if (endCurI >= nextColIndex && !firstCol) { + // make wider + setNodes( + editor, + { ...curCell, colSpan: curColSpan + 1 }, + { at: currentCellPath } + ); + } else { + // add new + const curRowPath = currentCellPath.slice(0, -1); + const curColPath = currentCellPath.at(-1)!; + const placementPath = [...curRowPath, curColPath + placementCorrection]; + + const row = getParentNode(editor, currentCellPath)!; + const rowElement = row[0] as TTableRowElement; + const emptyCell = { + ...createEmptyCell(editor, rowElement, newCellChildren, header), + rowSpan: curRowSpan, + colSpan: 1, + }; + insertElements(editor, emptyCell, { + at: placementPath, + select: !disableSelect && curRowIndex === currentRowIndex, + }); + } + }); + + withoutNormalizing(editor, () => { + const { colSizes } = tableNode; + + if (colSizes) { + let newColSizes = [ + ...colSizes.slice(0, nextColIndex), + 0, + ...colSizes.slice(nextColIndex), + ]; + + if (initialTableWidth) { + newColSizes[nextColIndex] = + colSizes[nextColIndex] ?? + colSizes[nextColIndex - 1] ?? + initialTableWidth / colSizes.length; + + const oldTotal = colSizes.reduce((a, b) => a + b, 0); + const newTotal = newColSizes.reduce((a, b) => a + b, 0); + const maxTotal = Math.max(oldTotal, initialTableWidth); + + if (newTotal > maxTotal) { + const factor = maxTotal / newTotal; + newColSizes = newColSizes.map((size) => + Math.max(minColumnWidth ?? 0, Math.floor(size * factor)) + ); + } + } + + setNodes( + editor, + { + colSizes: newColSizes, + }, + { + at: tablePath, + } + ); + } + }); +}; diff --git a/packages/table/src/merge/insertTableRow.ts b/packages/table/src/merge/insertTableRow.ts new file mode 100644 index 0000000000..e56f8d084e --- /dev/null +++ b/packages/table/src/merge/insertTableRow.ts @@ -0,0 +1,205 @@ +import { + findNode, + getBlockAbove, + getParentNode, + getPluginOptions, + getPluginType, + insertElements, + PlateEditor, + select, + setNodes, + TDescendant, + TElement, + Value, + withoutNormalizing, +} from '@udecode/plate-common'; +import { Path } from 'slate'; + +import { ELEMENT_TABLE, ELEMENT_TH, ELEMENT_TR } from '../createTablePlugin'; +import { getTableColumnCount } from '../queries'; +import { getColSpan } from '../queries/getColSpan'; +import { getRowSpan } from '../queries/getRowSpan'; +import { + TablePlugin, + TTableCellElement, + TTableElement, + TTableRowElement, +} from '../types'; +import { getCellTypes, getEmptyCellNode } from '../utils'; +import { findCellByIndexes } from './findCellByIndexes'; +import { getCellPath } from './getCellPath'; +import { getIndices } from './getIndices'; + +const createEmptyCell = ( + editor: PlateEditor, + row: TTableRowElement, + newCellChildren?: TDescendant[], + header?: boolean +) => { + const isHeaderRow = + header === undefined + ? (row as TElement).children.every( + (c) => c.type === getPluginType(editor, ELEMENT_TH) + ) + : header; + + return getEmptyCellNode(editor, { + header: isHeaderRow, + newCellChildren, + }); +}; + +export const insertTableRow = ( + editor: PlateEditor, + { + header, + fromRow, + at, + disableSelect, + }: { + header?: boolean; + fromRow?: Path; + /** + * Exact path of the row to insert the column at. + * Will overrule `fromRow`. + */ + at?: Path; + disableSelect?: boolean; + } = {} +) => { + const options = getPluginOptions(editor, ELEMENT_TABLE); + + const trEntry = fromRow + ? findNode(editor, { + at: fromRow, + match: { type: getPluginType(editor, ELEMENT_TR) }, + }) + : getBlockAbove(editor, { + match: { type: getPluginType(editor, ELEMENT_TR) }, + }); + if (!trEntry) return; + + const [, trPath] = trEntry; + + const tableEntry = getBlockAbove(editor, { + match: { type: getPluginType(editor, ELEMENT_TABLE) }, + at: trPath, + }); + if (!tableEntry) return; + const tableNode = tableEntry[0] as TTableElement; + + const { newCellChildren } = getPluginOptions( + editor, + ELEMENT_TABLE + ); + const cellEntry = findNode(editor, { + at: fromRow, + match: { type: getCellTypes(editor) }, + }); + if (!cellEntry) return; + const [cellNode, cellPath] = cellEntry; + const cellElement = cellNode as TTableCellElement; + const cellRowSpan = getRowSpan(cellElement); + const { row: cellRowIndex } = getIndices(options, cellElement)!; + + const rowPath = cellPath.at(-2)!; + const tablePath = cellPath.slice(0, -2)!; + + let nextRowIndex: number; + let checkingRowIndex: number; + let nextRowPath: number[]; + if (Path.isPath(at)) { + nextRowIndex = at.at(-1)!; + checkingRowIndex = cellRowIndex - 1; + nextRowPath = at; + } else { + nextRowIndex = cellRowIndex + cellRowSpan; + checkingRowIndex = cellRowIndex + cellRowSpan - 1; + nextRowPath = [...tablePath, rowPath + cellRowSpan]; + } + + const firstRow = nextRowIndex === 0; + if (firstRow) { + checkingRowIndex = 0; + } + + const colCount = getTableColumnCount(tableNode); + const affectedCellsSet = new Set(); + Array.from({ length: colCount }, (_, i) => i).forEach((cI) => { + const found = findCellByIndexes(editor, tableNode, checkingRowIndex, cI); + if (found) { + affectedCellsSet.add(found); + } + }); + const affectedCells = Array.from(affectedCellsSet) as TTableCellElement[]; + + const newRowChildren: TTableCellElement[] = []; + affectedCells.forEach((cur) => { + if (!cur) return; + + const curCell = cur as TTableCellElement; + const { row: curRowIndex, col: curColIndex } = getIndices( + options, + curCell + )!; + + const curRowSpan = getRowSpan(curCell); + const curColSpan = getColSpan(curCell); + const currentCellPath = getCellPath( + editor, + tableEntry, + curRowIndex, + curColIndex + ); + + const endCurI = curRowIndex + curRowSpan - 1; + if (endCurI >= nextRowIndex && !firstRow) { + // make higher + setNodes( + editor, + { ...curCell, rowSpan: curRowSpan + 1 }, + { at: currentCellPath } + ); + } else { + // add new + const row = getParentNode(editor, currentCellPath)!; + const rowElement = row[0] as TTableRowElement; + const emptyCell = createEmptyCell( + editor, + rowElement, + newCellChildren, + header + ) as TTableCellElement; + + newRowChildren.push({ + ...emptyCell, + colSpan: curColSpan, + rowSpan: 1, + }); + } + }); + + withoutNormalizing(editor, () => { + insertElements( + editor, + { + type: getPluginType(editor, ELEMENT_TR), + children: newRowChildren, + }, + { + at: nextRowPath, + } + ); + }); + + if (!disableSelect) { + const nextCellPath = cellPath; + if (Path.isPath(at)) { + nextCellPath[nextCellPath.length - 2] = at.at(-2)!; + } else { + nextCellPath[nextCellPath.length - 2] += cellRowSpan; + } + + select(editor, nextCellPath); + } +}; diff --git a/packages/table/src/merge/mergeTableCells.ts b/packages/table/src/merge/mergeTableCells.ts index 647fb3bb42..bf397a2f57 100644 --- a/packages/table/src/merge/mergeTableCells.ts +++ b/packages/table/src/merge/mergeTableCells.ts @@ -12,11 +12,11 @@ import { cloneDeep } from 'lodash'; import { ELEMENT_TABLE } from '../createTablePlugin'; import { getTableGridAbove } from '../queries'; -import { computeCellIndices } from '../queries/computeCellIndices'; import { getColSpan } from '../queries/getColSpan'; import { getRowSpan } from '../queries/getRowSpan'; import { TablePlugin, TTableCellElement, TTableElement } from '../types'; import { getEmptyCellNode } from '../utils'; +import { computeCellIndices } from './computeCellIndices'; /** * Merges multiple selected cells into one. diff --git a/packages/table/src/queries/getTableGridByRange.ts b/packages/table/src/queries/getTableGridByRange.ts index 4171cee1c4..083631bc8d 100644 --- a/packages/table/src/queries/getTableGridByRange.ts +++ b/packages/table/src/queries/getTableGridByRange.ts @@ -1,8 +1,6 @@ import { - findNode, - findNodePath, + getNode, getPluginOptions, - getPluginType, PlateEditor, TElement, TElementEntry, @@ -11,29 +9,9 @@ import { import { Range } from 'slate'; import { ELEMENT_TABLE } from '../createTablePlugin'; -import { - TablePlugin, - TTableCellElement, - TTableElement, - TTableRowElement, -} from '../types'; -import { getCellTypes } from '../utils'; +import { getTableGridByRange as getTableGridByRangeMerge } from '../merge/getTableGridByRange'; +import { TablePlugin, TTableElement } from '../types'; import { getEmptyTableNode } from '../utils/getEmptyTableNode'; -import { computeCellIndices } from './computeCellIndices'; -import { findCellByIndexes } from './findCellByIndexes'; -import { getIndices } from './getIndices'; -import { getIndicesWithSpans } from './getIndicesWithSpans'; - -export type FormatType = 'table' | 'cell' | 'all'; - -export interface TableGridEntries { - tableEntries: TElementEntry[]; - cellEntries: TElementEntry[]; -} - -export type GetTableGridReturnType = T extends 'all' - ? TableGridEntries - : TElementEntry[]; export interface GetTableGridByRangeOptions { at: Range; @@ -53,44 +31,29 @@ export const getTableGridByRange = ( editor: PlateEditor, { at, format = 'table' }: GetTableGridByRangeOptions ): TElementEntry[] => { - const options = getPluginOptions(editor, ELEMENT_TABLE); - - const startCellEntry = findNode(editor, { - at: (at as any).anchor.path, - match: { type: getCellTypes(editor) }, - })!; // TODO: improve typing - const endCellEntry = findNode(editor, { - at: (at as any).focus.path, - match: { type: getCellTypes(editor) }, - })!; - - const startCell = startCellEntry[0] as TTableCellElement; - const endCell = endCellEntry[0] as TTableCellElement; - - const startCellPath = (at as any).anchor.path; - const tablePath = startCellPath.slice(0, -2); + const { disableCellsMerging } = getPluginOptions( + editor, + ELEMENT_TABLE + ); + if (!disableCellsMerging) { + return getTableGridByRangeMerge(editor, { at, format }); + } - const tableEntry = findNode(editor, { - at: tablePath, - match: { type: getPluginType(editor, ELEMENT_TABLE) }, - })!; // TODO: improve typing - const realTable = tableEntry[0] as TTableElement; + const startCellPath = at.anchor.path; + const endCellPath = at.focus.path; - const { col: _startColIndex, row: _startRowIndex } = - getIndices(options, startCell) || - computeCellIndices(editor, realTable, startCell)!; - - const { row: _endRowIndex, col: _endColIndex } = getIndicesWithSpans( - getIndices(options, endCell) || - computeCellIndices(editor, realTable, endCell)!, - endCell - ); + const _startRowIndex = startCellPath.at(-2)!; + const _endRowIndex = endCellPath.at(-2)!; + const _startColIndex = startCellPath.at(-1)!; + const _endColIndex = endCellPath.at(-1)!; const startRowIndex = Math.min(_startRowIndex, _endRowIndex); const endRowIndex = Math.max(_startRowIndex, _endRowIndex); const startColIndex = Math.min(_startColIndex, _endColIndex); const endColIndex = Math.max(_startColIndex, _endColIndex); + const tablePath = startCellPath.slice(0, -2); + const relativeRowIndex = endRowIndex - startRowIndex; const relativeColIndex = endColIndex - startColIndex; @@ -100,55 +63,37 @@ export const getTableGridByRange = ( newCellChildren: [], }); - const cellEntries: TElementEntry[] = []; - const cellsSet = new WeakSet(); - let rowIndex = startRowIndex; let colIndex = startColIndex; + + const cellEntries: TElementEntry[] = []; + while (true) { - const cell = findCellByIndexes(editor, realTable, rowIndex, colIndex); - if (!cell) { - break; - } + const cellPath = tablePath.concat([rowIndex, colIndex]); - if (!cellsSet.has(cell)) { - cellsSet.add(cell); + const cell = getNode(editor, cellPath); + if (!cell) break; - const rows = table.children[rowIndex - startRowIndex] - .children as TElement[]; - rows[colIndex - startColIndex] = cell; + const rows = table.children[rowIndex - startRowIndex] + .children as TElement[]; - const cellPath = findNodePath(editor, cell)!; - cellEntries.push([cell, cellPath]); - } + rows[colIndex - startColIndex] = cell; + + cellEntries.push([cell, cellPath]); if (colIndex + 1 <= endColIndex) { - colIndex = colIndex + 1; + colIndex += 1; } else if (rowIndex + 1 <= endRowIndex) { colIndex = startColIndex; - rowIndex = rowIndex + 1; + rowIndex += 1; } else { break; } } - const formatType = (format as string) || 'table'; - - if (formatType === 'cell') { + if (format === 'cell') { return cellEntries; } - // clear redundant cells - table.children?.forEach((rowEl) => { - const rowElement = rowEl as TTableRowElement; - - const filteredChildren = rowElement.children?.filter((cellEl) => { - const cellElement = cellEl as TTableCellElement; - return !!cellElement?.children.length; - }); - - rowElement.children = filteredChildren; - }); - return [[table, tablePath]]; }; diff --git a/packages/table/src/transforms/deleteColumn.ts b/packages/table/src/transforms/deleteColumn.ts index fcd905f7c6..3f9aab2a90 100644 --- a/packages/table/src/transforms/deleteColumn.ts +++ b/packages/table/src/transforms/deleteColumn.ts @@ -1,5 +1,6 @@ import { getAboveNode, + getPluginOptions, getPluginType, PlateEditor, removeNodes, @@ -16,9 +17,18 @@ import { ELEMENT_TH, ELEMENT_TR, } from '../createTablePlugin'; -import { TTableElement } from '../types'; +import { deleteColumn as deleteColumnMerging } from '../merge/deleteColumn'; +import { TablePlugin, TTableElement } from '../types'; export const deleteColumn = (editor: PlateEditor) => { + const { disableCellsMerging } = getPluginOptions( + editor, + ELEMENT_TABLE + ); + if (!disableCellsMerging) { + return deleteColumnMerging(editor); + } + if ( someNode(editor, { match: { type: getPluginType(editor, ELEMENT_TABLE) }, diff --git a/packages/table/src/transforms/deleteRow.ts b/packages/table/src/transforms/deleteRow.ts index 703afed4ea..719d38dc3f 100644 --- a/packages/table/src/transforms/deleteRow.ts +++ b/packages/table/src/transforms/deleteRow.ts @@ -1,5 +1,6 @@ import { getAboveNode, + getPluginOptions, getPluginType, PlateEditor, removeNodes, @@ -8,9 +9,18 @@ import { } from '@udecode/plate-common'; import { ELEMENT_TABLE, ELEMENT_TR } from '../createTablePlugin'; -import { TTableElement } from '../types'; +import { deleteRow as deleteRowMerging } from '../merge/deleteRow'; +import { TablePlugin, TTableElement } from '../types'; export const deleteRow = (editor: PlateEditor) => { + const { disableCellsMerging } = getPluginOptions( + editor, + ELEMENT_TABLE + ); + if (!disableCellsMerging) { + return deleteRowMerging(editor); + } + if ( someNode(editor, { match: { type: getPluginType(editor, ELEMENT_TABLE) }, diff --git a/packages/table/src/transforms/insertTableColumn.ts b/packages/table/src/transforms/insertTableColumn.ts index fea3f48338..e4fac05a6a 100644 --- a/packages/table/src/transforms/insertTableColumn.ts +++ b/packages/table/src/transforms/insertTableColumn.ts @@ -13,18 +13,14 @@ import { import { Path } from 'slate'; import { ELEMENT_TABLE, ELEMENT_TH } from '../createTablePlugin'; +import { insertTableColumn as insertTableColumnMerging } from '../merge/insertTableColumn'; import { TablePlugin, TTableElement } from '../types'; import { getEmptyCellNode } from '../utils/getEmptyCellNode'; import { getCellTypes } from '../utils/index'; export const insertTableColumn = ( editor: PlateEditor, - { - disableSelect, - fromCell, - at, - header, - }: { + options: { header?: boolean; /** @@ -44,6 +40,16 @@ export const insertTableColumn = ( disableSelect?: boolean; } = {} ) => { + const { disableCellsMerging } = getPluginOptions( + editor, + ELEMENT_TABLE + ); + if (!disableCellsMerging) { + return insertTableColumnMerging(editor, options); + } + + const { disableSelect, fromCell, at, header } = options; + const cellEntry = fromCell ? findNode(editor, { at: fromCell, diff --git a/packages/table/src/transforms/insertTableRow.ts b/packages/table/src/transforms/insertTableRow.ts index 5204bd182b..dd33ac6ef2 100644 --- a/packages/table/src/transforms/insertTableRow.ts +++ b/packages/table/src/transforms/insertTableRow.ts @@ -13,17 +13,13 @@ import { import { Path } from 'slate'; import { ELEMENT_TABLE, ELEMENT_TH, ELEMENT_TR } from '../createTablePlugin'; +import { insertTableRow as insertTableRowMerging } from '../merge/insertTableRow'; import { TablePlugin } from '../types'; import { getCellTypes, getEmptyCellNode } from '../utils/index'; export const insertTableRow = ( editor: PlateEditor, - { - header, - fromRow, - at, - disableSelect, - }: { + options: { header?: boolean; fromRow?: Path; /** @@ -34,6 +30,16 @@ export const insertTableRow = ( disableSelect?: boolean; } = {} ) => { + const { disableCellsMerging } = getPluginOptions( + editor, + ELEMENT_TABLE + ); + if (!disableCellsMerging) { + return insertTableRowMerging(editor, options); + } + + const { header, fromRow, at, disableSelect } = options; + const trEntry = fromRow ? findNode(editor, { at: fromRow, diff --git a/packages/table/src/types.ts b/packages/table/src/types.ts index 02a3893dce..cd8eee7c4d 100644 --- a/packages/table/src/types.ts +++ b/packages/table/src/types.ts @@ -60,7 +60,12 @@ export interface TablePlugin { minColumnWidth?: number; /** - * For internal use. Keeps track of cell indices + * Enables / disabled cells merging functionality. + */ + disableCellsMerging?: boolean; + + /** + * For internal use. Keeps track of cell indices. Used only when disableCellsMerging is false. */ _cellIndices: TableStoreCellAttributes; }