import { Editor, findParentNode } from '@tiptap/core' import { CellSelection, type Rect, TableMap } from '@tiptap/pm/tables' 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 }