Skip to content
File

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

typescript185 lines
1import type { EditorView } from "@tiptap/pm/view";
2import { TableMap, moveTableColumn, moveTableRow } from "@tiptap/pm/tables";
3import { DRAG_THRESHOLD_PX } from "./constants";
4import { buildColumnEntries, findTableForWrapper, rowDropIndex, columnDropIndex } from "./dom";
5import { createOpenMenuState, type OpenMenuState } from "./state";
6 
7export type DragState =
8 | {
9 kind: "reorder-row";
10 sourceIndex: number;
11 tablePos: number;
12 wrapper: HTMLElement;
13 pointerId: number;
14 dropIndex: number | null;
15 moved: boolean;
16 startX: number;
17 startY: number;
18 }
19 | {
20 kind: "reorder-column";
21 sourceIndex: number;
22 tablePos: number;
23 wrapper: HTMLElement;
24 pointerId: number;
25 dropIndex: number | null;
26 moved: boolean;
27 startX: number;
28 startY: number;
29 };
30 
31export interface DragIndicatorState {
32 orientation: "row" | "column";
33 top: number;
34 left: number;
35 width: number;
36 height: number;
37}
38 
39export function createRowDragState(
40 view: EditorView,
41 wrapper: HTMLElement,
42 rowIndex: number,
43 pointerId: number,
44 startX: number,
45 startY: number,
46): DragState | null {
47 const info = findTableForWrapper(view, wrapper);
48 if (!info) return null;
49 
50 return {
51 kind: "reorder-row",
52 sourceIndex: rowIndex,
53 tablePos: info.pos,
54 wrapper,
55 pointerId,
56 dropIndex: null,
57 moved: false,
58 startX,
59 startY,
60 };
61}
62 
63export function createColumnDragState(
64 view: EditorView,
65 wrapper: HTMLElement,
66 logicalCol: number,
67 pointerId: number,
68 startX: number,
69 startY: number,
70): DragState | null {
71 const info = findTableForWrapper(view, wrapper);
72 if (!info) return null;
73 
74 return {
75 kind: "reorder-column",
76 sourceIndex: logicalCol,
77 tablePos: info.pos,
78 wrapper,
79 pointerId,
80 dropIndex: null,
81 moved: false,
82 startX,
83 startY,
84 };
85}
86 
87export function updateDragState(
88 view: EditorView,
89 state: DragState,
90 clientX: number,
91 clientY: number,
92 toLocal: (rect: DOMRect) => {
93 top: number;
94 left: number;
95 right: number;
96 bottom: number;
97 width: number;
98 height: number;
99 },
100): DragIndicatorState | null {
101 const table = state.wrapper.querySelector<HTMLTableElement>(":scope > table");
102 if (!table) return null;
103 
104 if (!state.moved) {
105 const dx = Math.abs(clientX - state.startX);
106 const dy = Math.abs(clientY - state.startY);
107 if (dx < DRAG_THRESHOLD_PX && dy < DRAG_THRESHOLD_PX) return null;
108 state.moved = true;
109 }
110 
111 const tableLocal = toLocal(table.getBoundingClientRect());
112 if (state.kind === "reorder-row") {
113 const rows = Array.from(table.querySelectorAll<HTMLTableRowElement>(":scope > tbody > tr"));
114 if (rows.length === 0) return null;
115 
116 const index = rowDropIndex(rows, clientY);
117 state.dropIndex = index;
118 const top =
119 index < rows.length
120 ? toLocal(rows[index].getBoundingClientRect()).top
121 : toLocal(rows[rows.length - 1].getBoundingClientRect()).bottom;
122 
123 return {
124 orientation: "row",
125 top: top - 1,
126 left: tableLocal.left,
127 width: tableLocal.width,
128 height: 2,
129 };
130 }
131 
132 const rows = Array.from(table.querySelectorAll<HTMLTableRowElement>(":scope > tbody > tr"));
133 if (rows.length === 0) return null;
134 
135 const columnEntries = buildColumnEntries(rows[0]);
136 if (columnEntries.length === 0) return null;
137 
138 const info = findTableForWrapper(view, state.wrapper);
139 if (!info) return null;
140 
141 const map = TableMap.get(info.node);
142 const index = columnDropIndex(columnEntries, map.width, clientX);
143 state.dropIndex = index;
144 
145 const left =
146 index >= map.width
147 ? toLocal(columnEntries[columnEntries.length - 1].domCell.getBoundingClientRect()).right
148 : toLocal(
149 (
150 columnEntries.find((entry) => entry.logicalCol === index) ?? columnEntries[0]
151 ).domCell.getBoundingClientRect(),
152 ).left;
153 
154 return {
155 orientation: "column",
156 top: tableLocal.top,
157 left: left - 1,
158 width: 2,
159 height: tableLocal.height,
160 };
161}
162 
163export function completeDrag(view: EditorView, state: DragState): OpenMenuState | null {
164 if (!state.moved) {
165 const table = view.state.doc.nodeAt(state.tablePos);
166 if (!table || table.type.spec.tableRole !== "table") return null;
167 return createOpenMenuState(state.kind === "reorder-row" ? "row" : "col", state.sourceIndex, state.tablePos, table);
168 }
169 
170 const dropIndex = state.dropIndex;
171 if (dropIndex === null || dropIndex === state.sourceIndex || dropIndex === state.sourceIndex + 1) {
172 return null;
173 }
174 
175 const target = dropIndex > state.sourceIndex ? dropIndex - 1 : dropIndex;
176 const pos = state.tablePos + 1;
177 if (state.kind === "reorder-row") {
178 moveTableRow({ from: state.sourceIndex, to: target, pos, select: true })(view.state, view.dispatch.bind(view));
179 return null;
180 }
181 
182 moveTableColumn({ from: state.sourceIndex, to: target, pos, select: true })(view.state, view.dispatch.bind(view));
183 return null;
184}