import { Editor, findParentNode } from '@tiptap/core'; import { EditorState, Selection, Transaction } from '@tiptap/pm/state'; import { CellSelection, type Rect, TableMap } from '@tiptap/pm/tables'; import { Node, ResolvedPos } from '@tiptap/pm/model'; import type { EditorView } from '@tiptap/pm/view'; import Table from './table.js'; export const isRectSelected = (rect: Rect) => (selection: CellSelection) => { const map = TableMap.get(selection.$anchorCell.node(-1)); const start = selection.$anchorCell.start(-1); const cells = map.cellsInRect(rect); const selectedCells = map.cellsInRect( map.rectBetween(selection.$anchorCell.pos - start, selection.$headCell.pos - start) ); for (let i = 0, count = cells.length; i < count; i += 1) { if (selectedCells.indexOf(cells[i]) === -1) { return false; } } return true; }; export const findTable = (selection: Selection) => findParentNode((node) => node.type.spec.tableRole && node.type.spec.tableRole === 'table')( selection ); export const isCellSelection = (selection: Selection): selection is CellSelection => selection instanceof CellSelection; export const isColumnSelected = (columnIndex: number) => (selection: Selection) => { if (isCellSelection(selection)) { const map = TableMap.get(selection.$anchorCell.node(-1)); return isRectSelected({ left: columnIndex, right: columnIndex + 1, top: 0, bottom: map.height })(selection); } return false; }; export const isRowSelected = (rowIndex: number) => (selection: Selection) => { if (isCellSelection(selection)) { const map = TableMap.get(selection.$anchorCell.node(-1)); return isRectSelected({ left: 0, right: map.width, top: rowIndex, bottom: rowIndex + 1 })(selection); } return false; }; export const isTableSelected = (selection: Selection) => { if (isCellSelection(selection)) { const map = TableMap.get(selection.$anchorCell.node(-1)); return isRectSelected({ left: 0, right: map.width, top: 0, bottom: map.height })(selection); } return false; }; export const getCellsInColumn = (columnIndex: number | number[]) => (selection: Selection) => { const table = findTable(selection); if (table) { const map = TableMap.get(table.node); const indexes = Array.isArray(columnIndex) ? columnIndex : Array.from([columnIndex]); return indexes.reduce( (acc, index) => { if (index >= 0 && index <= map.width - 1) { const cells = map.cellsInRect({ left: index, right: index + 1, top: 0, bottom: map.height }); return acc.concat( cells.map((nodePos) => { const node = table.node.nodeAt(nodePos); const pos = nodePos + table.start; return { pos, start: pos + 1, node }; }) ); } return acc; }, [] as { pos: number; start: number; node: Node | null | undefined }[] ); } return null; }; export const getCellsInRow = (rowIndex: number | number[]) => (selection: Selection) => { const table = findTable(selection); if (table) { const map = TableMap.get(table.node); const indexes = Array.isArray(rowIndex) ? rowIndex : Array.from([rowIndex]); return indexes.reduce( (acc, index) => { if (index >= 0 && index <= map.height - 1) { const cells = map.cellsInRect({ left: 0, right: map.width, top: index, bottom: index + 1 }); return acc.concat( cells.map((nodePos) => { const node = table.node.nodeAt(nodePos); const pos = nodePos + table.start; return { pos, start: pos + 1, node }; }) ); } return acc; }, [] as { pos: number; start: number; node: Node | null | undefined }[] ); } return null; }; export const getCellsInTable = (selection: Selection) => { const table = findTable(selection); if (table) { const map = TableMap.get(table.node); const cells = map.cellsInRect({ left: 0, right: map.width, top: 0, bottom: map.height }); return cells.map((nodePos) => { const node = table.node.nodeAt(nodePos); const pos = nodePos + table.start; return { pos, start: pos + 1, node }; }); } return null; }; export const findParentNodeClosestToPos = ( $pos: ResolvedPos, predicate: (node: Node) => boolean ) => { for (let i = $pos.depth; i > 0; i -= 1) { const node = $pos.node(i); if (predicate(node)) { return { pos: i > 0 ? $pos.before(i) : 0, start: $pos.start(i), depth: i, node }; } } return null; }; export const findCellClosestToPos = ($pos: ResolvedPos) => { const predicate = (node: Node) => node.type.spec.tableRole && /cell/i.test(node.type.spec.tableRole); return findParentNodeClosestToPos($pos, predicate); }; const select = (type: 'row' | 'column') => (index: number) => (tr: Transaction) => { const table = findTable(tr.selection); const isRowSelection = type === 'row'; if (table) { const map = TableMap.get(table.node); // Check if the index is valid if (index >= 0 && index < (isRowSelection ? map.height : map.width)) { const left = isRowSelection ? 0 : index; const top = isRowSelection ? index : 0; const right = isRowSelection ? map.width : index + 1; const bottom = isRowSelection ? index + 1 : map.height; const cellsInFirstRow = map.cellsInRect({ left, top, right: isRowSelection ? right : left + 1, bottom: isRowSelection ? top + 1 : bottom }); const cellsInLastRow = bottom - top === 1 ? cellsInFirstRow : map.cellsInRect({ left: isRowSelection ? left : right - 1, top: isRowSelection ? bottom - 1 : top, right, bottom }); const head = table.start + cellsInFirstRow[0]; const anchor = table.start + cellsInLastRow[cellsInLastRow.length - 1]; const $head = tr.doc.resolve(head); const $anchor = tr.doc.resolve(anchor); return tr.setSelection(new CellSelection($anchor, $head)); } } return tr; }; export const selectColumn = select('column'); export const selectRow = select('row'); export const selectTable = (tr: Transaction) => { const table = findTable(tr.selection); if (table) { const { map } = TableMap.get(table.node); if (map && map.length) { const head = table.start + map[0]; const anchor = table.start + map[map.length - 1]; const $head = tr.doc.resolve(head); const $anchor = tr.doc.resolve(anchor); return tr.setSelection(new CellSelection($anchor, $head)); } } return tr; }; export const isColumnGripSelected = ({ editor, view, state, from }: { editor: Editor; view: EditorView; state: EditorState; from: number; }) => { const domAtPos = view.domAtPos(from).node as HTMLElement; const nodeDOM = view.nodeDOM(from) as HTMLElement; const node = nodeDOM || domAtPos; if (!editor.isActive(Table.name) || !node || isTableSelected(state.selection)) { return false; } let container = node; while (container && !['TD', 'TH'].includes(container.tagName)) { container = container.parentElement!; } const gripColumn = container && container.querySelector && container.querySelector('a.grip-column.selected'); return !!gripColumn; }; export const isRowGripSelected = ({ editor, view, state, from }: { editor: Editor; view: EditorView; state: EditorState; from: number; }) => { const domAtPos = view.domAtPos(from).node as HTMLElement; const nodeDOM = view.nodeDOM(from) as HTMLElement; const node = nodeDOM || domAtPos; if (!editor.isActive(Table.name) || !node || isTableSelected(state.selection)) { return false; } let container = node; while (container && !['TD', 'TH'].includes(container.tagName)) { container = container.parentElement!; } const gripRow = container && container.querySelector && container.querySelector('a.grip-row.selected'); return !!gripRow; };