Skip to content
File

Blob: src/client/components/editor/extensions/table/dom.ts

typescript113 lines
1import type { EditorView } from "@tiptap/pm/view";
2import type { Node as PMNode } from "@tiptap/pm/model";
3 
4export interface ColumnEntry {
5 domCell: HTMLTableCellElement;
6 logicalCol: number;
7 colspan: number;
8}
9 
10export interface LocalRect {
11 top: number;
12 left: number;
13 right: number;
14 bottom: number;
15 width: number;
16 height: number;
17}
18 
19export function findTableForWrapper(view: EditorView, wrapper: HTMLElement): { pos: number; node: PMNode } | null {
20 let found: { pos: number; node: PMNode } | null = null;
21 view.state.doc.descendants((node, pos) => {
22 if (found) return false;
23 if (node.type.spec.tableRole !== "table") return true;
24 if (view.nodeDOM(pos) === wrapper) {
25 found = { pos, node };
26 }
27 return false;
28 });
29 return found;
30}
31 
32export function findTableElement(view: EditorView, tablePos: number): HTMLTableElement | null {
33 try {
34 const domAt = view.domAtPos(tablePos + 1);
35 let node: Node | null = domAt.node;
36 while (node) {
37 if (node instanceof HTMLTableElement) return node;
38 node = node.parentNode;
39 }
40 } catch {
41 return null;
42 }
43 return null;
44}
45 
46export function buildColumnEntries(firstRow: HTMLTableRowElement): ColumnEntry[] {
47 const entries: ColumnEntry[] = [];
48 let logicalCol = 0;
49 
50 const cells = Array.from(firstRow.querySelectorAll<HTMLTableCellElement>(":scope > td, :scope > th"));
51 for (const cell of cells) {
52 const colspanAttr = cell.getAttribute("colspan");
53 const colspan = colspanAttr ? Math.max(1, parseInt(colspanAttr, 10)) : 1;
54 entries.push({ domCell: cell, logicalCol, colspan });
55 logicalCol += colspan;
56 }
57 
58 return entries;
59}
60 
61export function findLogicalCellElement(
62 rows: HTMLTableRowElement[],
63 rowIndex: number,
64 colIndex: number,
65): HTMLTableCellElement | null {
66 const row = rows[rowIndex];
67 if (!row) return null;
68 
69 let logicalCol = 0;
70 const cells = Array.from(row.querySelectorAll<HTMLTableCellElement>(":scope > td, :scope > th"));
71 for (const cell of cells) {
72 const colspanAttr = cell.getAttribute("colspan");
73 const colspan = colspanAttr ? Math.max(1, parseInt(colspanAttr, 10)) : 1;
74 if (colIndex >= logicalCol && colIndex < logicalCol + colspan) return cell;
75 logicalCol += colspan;
76 }
77 
78 return null;
79}
80 
81export function rowDropIndex(rows: HTMLTableRowElement[], y: number): number {
82 if (rows.length === 0) return 0;
83 for (let index = 0; index < rows.length; index++) {
84 const rect = rows[index].getBoundingClientRect();
85 if (y < rect.top + rect.height / 2) return index;
86 }
87 return rows.length;
88}
89 
90export function columnDropIndex(entries: ColumnEntry[], totalCols: number, x: number): number {
91 if (entries.length === 0) return 0;
92 for (const entry of entries) {
93 const rect = entry.domCell.getBoundingClientRect();
94 if (x < rect.left + rect.width / 2) return entry.logicalCol;
95 }
96 return totalCols;
97}
98 
99export function createLocalRectConverter(host: HTMLElement): (rect: DOMRect) => LocalRect {
100 const rootRect = host.getBoundingClientRect();
101 const rootScrollTop = host.scrollTop;
102 const rootScrollLeft = host.scrollLeft;
103 
104 return (rect: DOMRect) => ({
105 top: rect.top - rootRect.top + rootScrollTop,
106 left: rect.left - rootRect.left + rootScrollLeft,
107 right: rect.right - rootRect.left + rootScrollLeft,
108 bottom: rect.bottom - rootRect.top + rootScrollTop,
109 width: rect.width,
110 height: rect.height,
111 });
112}