hensei-web/src/lib/components/edra/extensions/table/utils.ts
Justin Edmund 2792279f9a add edra tiptap editor component
copied from edra library with svelte 5 fix for onTransaction callback
2025-12-21 15:12:51 -08:00

322 lines
7.6 KiB
TypeScript

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;
};