smartrte-react 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/QuillEditor.d.ts +8 -0
  2. package/dist/QuillEditor.js +34 -0
  3. package/dist/app.d.ts +6 -0
  4. package/dist/app.js +6 -0
  5. package/dist/blots/CommentBlot.d.ts +8 -0
  6. package/dist/blots/CommentBlot.js +17 -0
  7. package/dist/blots/FormulaBlot.d.ts +12 -0
  8. package/dist/blots/FormulaBlot.js +36 -0
  9. package/dist/blots/MediaBlot.d.ts +11 -0
  10. package/dist/blots/MediaBlot.js +37 -0
  11. package/dist/blots/TableBlot.d.ts +10 -0
  12. package/dist/blots/TableBlot.js +54 -0
  13. package/dist/blots/index.d.ts +5 -0
  14. package/dist/blots/index.js +12 -0
  15. package/dist/components/ClassicEditor.d.ts +10 -0
  16. package/dist/components/ClassicEditor.js +1066 -0
  17. package/dist/components/DiagramEditor.d.ts +5 -0
  18. package/dist/components/DiagramEditor.js +73 -0
  19. package/dist/components/FormulaEditor.d.ts +6 -0
  20. package/dist/components/FormulaEditor.js +86 -0
  21. package/dist/components/InfoBox.d.ts +7 -0
  22. package/dist/components/InfoBox.js +18 -0
  23. package/dist/components/MCQBlock.d.ts +13 -0
  24. package/dist/components/MCQBlock.js +29 -0
  25. package/dist/components/SmartEditor.d.ts +0 -0
  26. package/dist/components/SmartEditor.js +1 -0
  27. package/dist/components/SmartTable.d.ts +22 -0
  28. package/dist/components/SmartTable.js +629 -0
  29. package/dist/components/TableContextMenu.d.ts +11 -0
  30. package/dist/components/TableContextMenu.js +15 -0
  31. package/dist/components/TableInsertDialog.d.ts +7 -0
  32. package/dist/components/TableInsertDialog.js +42 -0
  33. package/dist/hooks/useEditorSync.d.ts +5 -0
  34. package/dist/hooks/useEditorSync.js +53 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/smart-editor.d.ts +0 -0
  38. package/dist/smart-editor.js +1 -0
  39. package/dist/standalone/classic-editor-embed.d.ts +12 -0
  40. package/dist/standalone/classic-editor-embed.js +108 -0
  41. package/dist/standalone/editor.js +241 -0
  42. package/package.json +46 -0
@@ -0,0 +1,629 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useMemo, useRef, useState, useEffect, } from "react";
3
+ export function SmartTable({ editor, tableNode, tableIdx, onChange, onCaretUpdate, }) {
4
+ const containerRef = useRef(null);
5
+ const draggingRef = useRef(false);
6
+ const hasDraggedRef = useRef(false);
7
+ const suppressNextClickClearRef = useRef(false);
8
+ const dragStartCellRef = useRef(null);
9
+ const dragStartPointRef = useRef(null);
10
+ const [focused, setFocused] = useState(null);
11
+ const cellRefs = useRef({});
12
+ const pendingCaret = useRef(null);
13
+ const debounceTimer = useRef(null);
14
+ const [selection, setSelection] = useState({ start: null, end: null, active: false });
15
+ const [menu, setMenu] = useState(null);
16
+ const normSel = useMemo(() => {
17
+ if (!selection.start || !selection.end)
18
+ return null;
19
+ const sr = Math.min(selection.start.r, selection.end.r);
20
+ const sc = Math.min(selection.start.c, selection.end.c);
21
+ const er = Math.max(selection.start.r, selection.end.r);
22
+ const ec = Math.max(selection.start.c, selection.end.c);
23
+ return { sr, sc, er, ec };
24
+ }, [selection]);
25
+ const clearMenu = useCallback(() => setMenu(null), []);
26
+ const onCellEdit = useCallback((r, c, text) => {
27
+ if (r < 0 ||
28
+ r >= tableNode.rows.length ||
29
+ c < 0 ||
30
+ c >= (tableNode.rows[r]?.cells?.length ?? 0)) {
31
+ console.warn("Invalid cell index onCellEdit", { tableIdx, r, c });
32
+ return;
33
+ }
34
+ if (typeof editor.set_cell_text_at === "function") {
35
+ editor.set_cell_text_at(tableIdx, r, c, text);
36
+ }
37
+ else {
38
+ editor.set_cell_text(r, c, text);
39
+ }
40
+ if (debounceTimer.current)
41
+ window.clearTimeout(debounceTimer.current);
42
+ debounceTimer.current = window.setTimeout(() => {
43
+ onChange && onChange();
44
+ debounceTimer.current = null;
45
+ }, 150);
46
+ }, [editor, onChange, tableIdx]);
47
+ const onResizeCol = useCallback((c, delta) => {
48
+ const current = tableNode.column_widths?.[c] ?? 120;
49
+ if (typeof editor.set_column_width_at === "function") {
50
+ editor.set_column_width_at(tableIdx, c, Math.max(60, current + delta));
51
+ }
52
+ else {
53
+ editor.set_column_width(c, Math.max(60, current + delta));
54
+ }
55
+ onChange && onChange();
56
+ }, [editor, tableNode, onChange, tableIdx]);
57
+ const onResizeRow = useCallback((r, delta) => {
58
+ const tr = containerRef.current?.querySelector(`tr[data-r='${r}']`);
59
+ const current = tr?.getBoundingClientRect().height || 24;
60
+ if (typeof editor.set_row_height_specific === "function") {
61
+ editor.set_row_height_specific(tableIdx, r, Math.max(18, Math.round(current + delta)));
62
+ }
63
+ else {
64
+ editor.set_row_height(r, Math.max(18, Math.round(current + delta)));
65
+ }
66
+ onChange && onChange();
67
+ }, [editor, onChange, tableIdx]);
68
+ const isSelected = useCallback((r, c) => {
69
+ if (!normSel)
70
+ return false;
71
+ return (r >= normSel.sr && r <= normSel.er && c >= normSel.sc && c <= normSel.ec);
72
+ }, [normSel]);
73
+ const openContextMenu = useCallback((e, r, c) => {
74
+ e.preventDefault();
75
+ const rect = containerRef.current?.getBoundingClientRect();
76
+ let x = e.clientX - (rect?.left ?? 0);
77
+ let y = e.clientY - (rect?.top ?? 0);
78
+ const viewportW = rect?.width ?? window.innerWidth;
79
+ const viewportH = rect?.height ?? window.innerHeight;
80
+ const menuW = 240;
81
+ const menuH = 260;
82
+ if (x + menuW > viewportW)
83
+ x = Math.max(0, viewportW - menuW - 8);
84
+ if (y + menuH > viewportH)
85
+ y = Math.max(0, viewportH - menuH - 8);
86
+ // If right-click is outside current selection, move selection to the clicked cell
87
+ const inside = !!(normSel &&
88
+ r >= normSel.sr &&
89
+ r <= normSel.er &&
90
+ c >= normSel.sc &&
91
+ c <= normSel.ec);
92
+ if (!inside) {
93
+ setSelection({ start: { r, c }, end: { r, c }, active: false });
94
+ }
95
+ setMenu({ x, y, r, c });
96
+ }, [normSel]);
97
+ const onMouseDownCell = useCallback((e, r, c) => {
98
+ // Only start drag-selection on left click
99
+ if (e.button !== 0)
100
+ return;
101
+ // Shift+click extends selection to here
102
+ if (e.shiftKey && selection.start) {
103
+ setSelection((s) => ({ start: s.start, end: { r, c }, active: false }));
104
+ return;
105
+ }
106
+ draggingRef.current = true;
107
+ hasDraggedRef.current = false;
108
+ dragStartCellRef.current = { r, c };
109
+ dragStartPointRef.current = { x: e.clientX, y: e.clientY };
110
+ const prevUserSelect = document.body.style.userSelect;
111
+ document.body.style.userSelect = "none";
112
+ const onMove = (ev) => {
113
+ if (!draggingRef.current)
114
+ return;
115
+ const start = dragStartPointRef.current;
116
+ if (!start)
117
+ return;
118
+ const dx = Math.abs(ev.clientX - start.x);
119
+ const dy = Math.abs(ev.clientY - start.y);
120
+ const moved = dx + dy > 3; // small threshold to differentiate click vs drag
121
+ if (!hasDraggedRef.current && moved) {
122
+ hasDraggedRef.current = true;
123
+ const startCell = dragStartCellRef.current || { r, c };
124
+ setSelection({
125
+ start: { r: startCell.r, c: startCell.c },
126
+ end: { r: startCell.r, c: startCell.c },
127
+ active: true,
128
+ });
129
+ }
130
+ const target = ev.target;
131
+ const td = target.closest("td[data-r][data-c]");
132
+ if (td) {
133
+ const rr = parseInt(td.dataset.r || "0", 10);
134
+ const cc = parseInt(td.dataset.c || "0", 10);
135
+ if (hasDraggedRef.current) {
136
+ setSelection((s) => ({ ...s, end: { r: rr, c: cc } }));
137
+ }
138
+ }
139
+ };
140
+ const onUp = () => {
141
+ draggingRef.current = false;
142
+ setSelection((s) => s && s.start && s.end ? { ...s, active: false } : s);
143
+ document.body.style.userSelect = prevUserSelect;
144
+ window.removeEventListener("mousemove", onMove);
145
+ window.removeEventListener("mouseup", onUp);
146
+ // If it was just a simple click (no drag), clear selection
147
+ if (!hasDraggedRef.current) {
148
+ setSelection({ start: null, end: null, active: false });
149
+ }
150
+ else {
151
+ // A drag selection occurred; avoid container onClick clearing it
152
+ suppressNextClickClearRef.current = true;
153
+ }
154
+ };
155
+ window.addEventListener("mousemove", onMove);
156
+ window.addEventListener("mouseup", onUp);
157
+ }, [selection.start]);
158
+ // Clear selection when clicking anywhere outside the table container
159
+ useEffect(() => {
160
+ const onDocMouseDown = (e) => {
161
+ const container = containerRef.current;
162
+ if (!container)
163
+ return;
164
+ if (!container.contains(e.target)) {
165
+ setSelection({ start: null, end: null, active: false });
166
+ }
167
+ };
168
+ document.addEventListener("mousedown", onDocMouseDown, true);
169
+ return () => document.removeEventListener("mousedown", onDocMouseDown, true);
170
+ }, []);
171
+ const applyBackground = useCallback((hex) => {
172
+ if (!normSel)
173
+ return;
174
+ for (let r = normSel.sr; r <= normSel.er; r++) {
175
+ for (let c = normSel.sc; c <= normSel.ec; c++) {
176
+ if (typeof editor.set_cell_style_at === "function") {
177
+ editor.set_cell_style_at(tableIdx, r, c, JSON.stringify({ background: hex }));
178
+ }
179
+ else {
180
+ editor.set_cell_style(r, c, JSON.stringify({ background: hex }));
181
+ }
182
+ }
183
+ }
184
+ clearMenu();
185
+ onChange && onChange();
186
+ }, [editor, normSel, clearMenu, tableIdx]);
187
+ const toggleBorder = useCallback(() => {
188
+ if (!normSel)
189
+ return;
190
+ for (let r = normSel.sr; r <= normSel.er; r++) {
191
+ for (let c = normSel.sc; c <= normSel.ec; c++) {
192
+ if (typeof editor.set_cell_style_at === "function") {
193
+ editor.set_cell_style_at(tableIdx, r, c, JSON.stringify({ border: { color: "#000", width_px: 1 } }));
194
+ }
195
+ else {
196
+ editor.set_cell_style(r, c, JSON.stringify({ border: { color: "#000", width_px: 1 } }));
197
+ }
198
+ }
199
+ }
200
+ clearMenu();
201
+ onChange && onChange();
202
+ }, [editor, normSel, clearMenu, tableIdx]);
203
+ const mergeSelected = useCallback(() => {
204
+ if (!normSel)
205
+ return;
206
+ if (typeof editor.merge_cells_at === "function") {
207
+ editor.merge_cells_at(tableIdx, normSel.sr, normSel.sc, normSel.er, normSel.ec);
208
+ }
209
+ else {
210
+ editor.merge_cells(normSel.sr, normSel.sc, normSel.er, normSel.ec);
211
+ }
212
+ clearMenu();
213
+ onChange && onChange();
214
+ }, [editor, normSel, clearMenu, tableIdx]);
215
+ const splitCell = useCallback(() => {
216
+ if (!menu)
217
+ return;
218
+ if (typeof editor.split_cell_at === "function") {
219
+ editor.split_cell_at(tableIdx, menu.r, menu.c);
220
+ }
221
+ else {
222
+ editor.split_cell(menu.r, menu.c);
223
+ }
224
+ clearMenu();
225
+ onChange && onChange();
226
+ }, [editor, menu, clearMenu, tableIdx]);
227
+ const insertRowAbove = useCallback(() => {
228
+ if (!menu)
229
+ return;
230
+ if (typeof editor.add_row_at === "function") {
231
+ editor.add_row_at(tableIdx, menu.r);
232
+ }
233
+ else {
234
+ editor.add_row(menu.r);
235
+ }
236
+ clearMenu();
237
+ onChange && onChange();
238
+ }, [editor, menu, onChange, tableIdx]);
239
+ const insertRowBelow = useCallback(() => {
240
+ if (!menu)
241
+ return;
242
+ if (typeof editor.add_row_at === "function") {
243
+ editor.add_row_at(tableIdx, menu.r + 1);
244
+ }
245
+ else {
246
+ editor.add_row(menu.r + 1);
247
+ }
248
+ clearMenu();
249
+ onChange && onChange();
250
+ }, [editor, menu, onChange, tableIdx]);
251
+ const insertColLeft = useCallback(() => {
252
+ if (!menu)
253
+ return;
254
+ if (typeof editor.add_col_at === "function") {
255
+ editor.add_col_at(tableIdx, menu.c);
256
+ }
257
+ else {
258
+ editor.add_col(menu.c);
259
+ }
260
+ clearMenu();
261
+ onChange && onChange();
262
+ }, [editor, menu, onChange, tableIdx]);
263
+ const insertColRight = useCallback(() => {
264
+ if (!menu)
265
+ return;
266
+ if (typeof editor.add_col_at === "function") {
267
+ editor.add_col_at(tableIdx, menu.c + 1);
268
+ }
269
+ else {
270
+ editor.add_col(menu.c + 1);
271
+ }
272
+ clearMenu();
273
+ onChange && onChange();
274
+ }, [editor, menu, onChange, tableIdx]);
275
+ const deleteRow = useCallback(() => {
276
+ if (!menu)
277
+ return;
278
+ if (typeof editor.delete_row_at === "function") {
279
+ editor.delete_row_at(tableIdx, menu.r);
280
+ }
281
+ else {
282
+ editor.delete_row(menu.r);
283
+ }
284
+ clearMenu();
285
+ onChange && onChange();
286
+ }, [editor, menu, onChange, tableIdx]);
287
+ const deleteCol = useCallback(() => {
288
+ if (!menu)
289
+ return;
290
+ if (typeof editor.delete_col_at === "function") {
291
+ editor.delete_col_at(tableIdx, menu.c);
292
+ }
293
+ else {
294
+ editor.delete_col(menu.c);
295
+ }
296
+ clearMenu();
297
+ onChange && onChange();
298
+ }, [editor, menu, onChange, tableIdx]);
299
+ const toggleFreezeHeader = useCallback(() => {
300
+ const next = !tableNode.freeze_header;
301
+ if (typeof editor.set_freeze_at === "function") {
302
+ editor.set_freeze_at(tableIdx, next, !!tableNode.freeze_first_col);
303
+ }
304
+ else {
305
+ editor.set_freeze(next, !!tableNode.freeze_first_col);
306
+ }
307
+ clearMenu();
308
+ onChange && onChange();
309
+ }, [editor, tableNode, onChange, tableIdx]);
310
+ const toggleFreezeFirstCol = useCallback(() => {
311
+ const next = !tableNode.freeze_first_col;
312
+ if (typeof editor.set_freeze_at === "function") {
313
+ editor.set_freeze_at(tableIdx, !!tableNode.freeze_header, next);
314
+ }
315
+ else {
316
+ editor.set_freeze(!!tableNode.freeze_header, next);
317
+ }
318
+ clearMenu();
319
+ onChange && onChange();
320
+ }, [editor, tableNode, onChange, tableIdx]);
321
+ const pivot = useMemo(() => {
322
+ if (selection.start)
323
+ return selection.start;
324
+ if (focused)
325
+ return focused;
326
+ return { r: 0, c: 0 };
327
+ }, [selection.start, focused]);
328
+ const applyTextStyleToSelection = useCallback((style) => {
329
+ if (!normSel)
330
+ return;
331
+ for (let r = normSel.sr; r <= normSel.er; r++) {
332
+ for (let c = normSel.sc; c <= normSel.ec; c++) {
333
+ const t = tableNode.rows[r]?.cells?.[c]?.text || "";
334
+ if (typeof editor.set_cell_text_style_at === "function") {
335
+ editor.set_cell_text_style_at(tableIdx, r, c, 0, t.length, JSON.stringify(style));
336
+ }
337
+ else {
338
+ editor.set_cell_text_style(r, c, 0, t.length, JSON.stringify(style));
339
+ }
340
+ }
341
+ }
342
+ onChange && onChange();
343
+ }, [editor, normSel, onChange, tableNode, tableIdx]);
344
+ return (_jsxs("div", { ref: containerRef, style: {
345
+ position: "relative",
346
+ overflow: "auto",
347
+ border: "1px solid #ccc",
348
+ margin: "8px 0",
349
+ }, onClick: (e) => {
350
+ clearMenu();
351
+ if (suppressNextClickClearRef.current) {
352
+ suppressNextClickClearRef.current = false;
353
+ return;
354
+ }
355
+ const target = e.target;
356
+ // If clicking outside any td/cell content, clear selection as well
357
+ if (!target.closest("td[data-r][data-c]")) {
358
+ setSelection({ start: null, end: null, active: false });
359
+ }
360
+ }, children: [_jsxs("div", { style: {
361
+ position: "sticky",
362
+ top: 0,
363
+ background: "#fff",
364
+ zIndex: 6,
365
+ display: "flex",
366
+ gap: 8,
367
+ padding: 6,
368
+ borderBottom: "1px solid #eee",
369
+ }, children: [_jsx("button", { onClick: mergeSelected, disabled: !normSel, children: "Merge" }), _jsx("button", { onClick: splitCell, disabled: !normSel, children: "Split" }), _jsx("button", { onClick: () => insertRowAbove(), disabled: !pivot, children: "+ Row \u2191" }), _jsx("button", { onClick: () => insertRowBelow(), disabled: !pivot, children: "+ Row \u2193" }), _jsx("button", { onClick: () => insertColLeft(), disabled: !pivot, children: "+ Col \u2190" }), _jsx("button", { onClick: () => insertColRight(), disabled: !pivot, children: "+ Col \u2192" }), _jsx("button", { onClick: () => deleteRow(), disabled: !pivot, children: "\u2212 Row" }), _jsx("button", { onClick: () => deleteCol(), disabled: !pivot, children: "\u2212 Col" }), _jsx("button", { onClick: toggleFreezeHeader, children: tableNode.freeze_header ? "Unfreeze header" : "Freeze header" }), _jsx("button", { onClick: toggleFreezeFirstCol, children: tableNode.freeze_first_col ? "Unfreeze 1st col" : "Freeze 1st col" }), _jsx("span", { style: { width: 1, background: "#eee", margin: "0 4px" } }), _jsx("button", { onClick: () => applyTextStyleToSelection({ bold: true }), children: "B" }), _jsx("button", { onClick: () => applyTextStyleToSelection({ italic: true }), children: "I" }), _jsx("button", { onClick: () => applyTextStyleToSelection({ underline: true }), children: "U" }), _jsxs("label", { style: { display: "inline-flex", alignItems: "center", gap: 4 }, children: ["A", _jsx("input", { type: "color", onChange: (e) => applyTextStyleToSelection({ color: e.target.value }) })] }), _jsxs("label", { style: { display: "inline-flex", alignItems: "center", gap: 4 }, children: ["\u2327", _jsx("input", { type: "color", onChange: (e) => applyBackground(e.target.value) })] }), _jsxs("select", { defaultValue: 14, onChange: (e) => applyTextStyleToSelection({ font_size_px: Number(e.target.value) }), title: "Font size", children: [_jsx("option", { value: 12, children: "12" }), _jsx("option", { value: 14, children: "14" }), _jsx("option", { value: 16, children: "16" }), _jsx("option", { value: 18, children: "18" }), _jsx("option", { value: 24, children: "24" })] })] }), _jsx("button", { "aria-label": "Remove table", onClick: (e) => {
370
+ e.stopPropagation();
371
+ try {
372
+ editor.delete_node?.(tableIdx);
373
+ }
374
+ catch { }
375
+ onChange && onChange();
376
+ }, style: {
377
+ position: "absolute",
378
+ right: 6,
379
+ top: 6,
380
+ width: 20,
381
+ height: 20,
382
+ borderRadius: 10,
383
+ border: "1px solid #ccc",
384
+ background: "#fff",
385
+ cursor: "pointer",
386
+ lineHeight: "18px",
387
+ textAlign: "center",
388
+ zIndex: 5,
389
+ }, title: "Remove table", children: "\u00D7" }), _jsx("table", { style: { borderCollapse: "collapse", width: "100%" }, children: _jsx("tbody", { children: tableNode.rows.map((row, rIdx) => (_jsx("tr", { "data-r": rIdx, style: { height: row.height_px }, children: row.cells.map((cell, cIdx) => {
390
+ if (cell.placeholder)
391
+ return null;
392
+ const width = tableNode.column_widths?.[cIdx] ?? 120;
393
+ const selected = isSelected(rIdx, cIdx);
394
+ return (_jsxs("td", { "data-r": rIdx, "data-c": cIdx, colSpan: cell.colspan, rowSpan: cell.rowspan, style: {
395
+ minWidth: width,
396
+ border: "1px solid #ddd",
397
+ padding: "6px",
398
+ background: cell.style?.background || "white",
399
+ position: "relative",
400
+ color: cell.style?.color || "black",
401
+ userSelect: selection.active ? "none" : undefined,
402
+ }, onMouseDown: (e) => onMouseDownCell(e, rIdx, cIdx), onContextMenu: (e) => openContextMenu(e, rIdx, cIdx), children: [_jsx("div", { contentEditable: true, suppressContentEditableWarning: true, style: {
403
+ outline: "none",
404
+ minHeight: 20,
405
+ whiteSpace: "pre-wrap",
406
+ direction: "ltr",
407
+ unicodeBidi: "plaintext",
408
+ textAlign: "left",
409
+ }, ref: (el) => {
410
+ cellRefs.current[`${rIdx}:${cIdx}`] = el;
411
+ }, onFocus: (e) => {
412
+ setFocused({ r: rIdx, c: cIdx });
413
+ // Normalize the live DOM to model value when entering focus
414
+ const currentDom = e.currentTarget.innerText || "";
415
+ const modelText = cell.text || "";
416
+ if (currentDom !== modelText) {
417
+ e.currentTarget.innerText = modelText;
418
+ }
419
+ const sel = window.getSelection();
420
+ const off = (sel &&
421
+ sel.anchorNode &&
422
+ (e.currentTarget.contains(sel.anchorNode)
423
+ ? sel.anchorOffset
424
+ : (e.currentTarget.textContent || "").length)) ||
425
+ 0;
426
+ onCaretUpdate &&
427
+ onCaretUpdate({
428
+ tableIdx,
429
+ r: rIdx,
430
+ c: cIdx,
431
+ offset: off,
432
+ });
433
+ }, onBlur: (e) => {
434
+ // Persist latest text and release focus state
435
+ const latest = e.currentTarget.innerText || "";
436
+ onCellEdit(rIdx, cIdx, latest);
437
+ setFocused((f) => f && f.r === rIdx && f.c === cIdx ? null : f);
438
+ // Clear pending caret; allow focus to move to the new cell naturally
439
+ pendingCaret.current = null;
440
+ },
441
+ // Reduce state churn while typing; caret context is captured on focus
442
+ onKeyDown: (e) => {
443
+ if (e.key === "Enter") {
444
+ if (e.shiftKey) {
445
+ // newline inside cell
446
+ e.preventDefault();
447
+ e.stopPropagation();
448
+ if (rIdx < 0 ||
449
+ rIdx >= tableNode.rows.length ||
450
+ cIdx < 0 ||
451
+ cIdx >= (tableNode.rows[rIdx]?.cells?.length ?? 0)) {
452
+ console.warn("Invalid cell index", {
453
+ tableIdx,
454
+ rIdx,
455
+ cIdx,
456
+ });
457
+ return;
458
+ }
459
+ const el = e.currentTarget;
460
+ const text = el.innerText || "";
461
+ const selection = window.getSelection();
462
+ const rawCaret = selection &&
463
+ selection.anchorNode &&
464
+ el.contains(selection.anchorNode)
465
+ ? selection.anchorOffset ?? text.length
466
+ : text.length;
467
+ const safeCaret = Math.min(Math.max(0, rawCaret), text.length);
468
+ const next = text.slice(0, safeCaret) +
469
+ "\n" +
470
+ text.slice(safeCaret);
471
+ try {
472
+ // Reflect the change in the live DOM so the newline is visible immediately
473
+ el.innerText = next;
474
+ // Move caret after the inserted newline
475
+ const textNode = el.firstChild;
476
+ const desiredOffset = Math.min(safeCaret + 1, textNode?.textContent?.length ?? next.length);
477
+ if (textNode) {
478
+ const range = document.createRange();
479
+ range.setStart(textNode, desiredOffset);
480
+ range.collapse(true);
481
+ const sel = window.getSelection();
482
+ sel?.removeAllRanges();
483
+ sel?.addRange(range);
484
+ }
485
+ pendingCaret.current = {
486
+ r: rIdx,
487
+ c: cIdx,
488
+ offset: desiredOffset,
489
+ };
490
+ onCaretUpdate &&
491
+ onCaretUpdate({
492
+ tableIdx,
493
+ r: rIdx,
494
+ c: cIdx,
495
+ offset: desiredOffset,
496
+ });
497
+ // Persist via debounced path so we don't fight React re-renders
498
+ onCellEdit(rIdx, cIdx, next);
499
+ }
500
+ catch (err) {
501
+ console.error("Failed to set_cell_text", {
502
+ tableIdx,
503
+ rIdx,
504
+ cIdx,
505
+ next,
506
+ err,
507
+ });
508
+ }
509
+ }
510
+ else {
511
+ // move to below cell
512
+ e.preventDefault();
513
+ if (rIdx < tableNode.rows.length - 1) {
514
+ onCaretUpdate({
515
+ tableIdx,
516
+ r: rIdx + 1,
517
+ c: cIdx,
518
+ offset: 0,
519
+ });
520
+ }
521
+ else {
522
+ editor.insert_table_row(tableIdx, tableNode.rows.length);
523
+ onCaretUpdate({
524
+ tableIdx,
525
+ r: rIdx + 1,
526
+ c: cIdx,
527
+ offset: 0,
528
+ });
529
+ onChange();
530
+ }
531
+ }
532
+ }
533
+ }, onInput: (e) => {
534
+ const sel = window.getSelection();
535
+ const off = sel &&
536
+ sel.anchorNode &&
537
+ e.currentTarget.contains(sel.anchorNode)
538
+ ? sel.anchorOffset || 0
539
+ : (e.currentTarget.textContent || "").length;
540
+ pendingCaret.current = {
541
+ r: rIdx,
542
+ c: cIdx,
543
+ offset: off,
544
+ };
545
+ // Emit caret context upwards so formula insertion knows table cell context
546
+ onCaretUpdate &&
547
+ onCaretUpdate({
548
+ tableIdx,
549
+ r: rIdx,
550
+ c: cIdx,
551
+ offset: off,
552
+ });
553
+ }, children: focused && focused.r === rIdx && focused.c === cIdx
554
+ ? null
555
+ : cell.text }), selected && (_jsx("div", { style: {
556
+ position: "absolute",
557
+ inset: 0,
558
+ background: "rgba(30, 144, 255, 0.2)",
559
+ pointerEvents: "none",
560
+ outline: "2px solid #1e90ff",
561
+ outlineOffset: -2,
562
+ } })), _jsx("div", { style: {
563
+ position: "absolute",
564
+ right: 0,
565
+ top: 0,
566
+ bottom: 0,
567
+ width: 6,
568
+ cursor: "col-resize",
569
+ }, onMouseDown: (e) => {
570
+ const startX = e.clientX;
571
+ const onMove = (ev) => {
572
+ const delta = ev.clientX - startX;
573
+ onResizeCol(cIdx, delta);
574
+ };
575
+ const onUp = () => {
576
+ window.removeEventListener("mousemove", onMove);
577
+ window.removeEventListener("mouseup", onUp);
578
+ };
579
+ window.addEventListener("mousemove", onMove);
580
+ window.addEventListener("mouseup", onUp);
581
+ } }), cIdx === row.cells.length - 1 && (_jsx("div", { style: {
582
+ position: "absolute",
583
+ left: 0,
584
+ right: 0,
585
+ bottom: -3,
586
+ height: 6,
587
+ cursor: "row-resize",
588
+ }, onMouseDown: (e) => {
589
+ const startY = e.clientY;
590
+ const onMove = (ev) => {
591
+ const delta = ev.clientY - startY;
592
+ onResizeRow(rIdx, delta);
593
+ };
594
+ const onUp = () => {
595
+ window.removeEventListener("mousemove", onMove);
596
+ window.removeEventListener("mouseup", onUp);
597
+ };
598
+ window.addEventListener("mousemove", onMove);
599
+ window.addEventListener("mouseup", onUp);
600
+ } }))] }, cIdx));
601
+ }) }, rIdx))) }) }), menu && (_jsxs("div", { style: {
602
+ position: "absolute",
603
+ left: menu.x,
604
+ top: menu.y,
605
+ background: "#fff",
606
+ boxShadow: "0 8px 24px rgba(0,0,0,0.18)",
607
+ borderRadius: 8,
608
+ padding: 10,
609
+ zIndex: 50,
610
+ width: 240,
611
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, fontSize: 12, marginBottom: 8 }, children: "Cell actions" }), _jsx("div", { style: { display: "flex", gap: 8, marginBottom: 10 }, children: [
612
+ "#ffffff",
613
+ "#fff2cc",
614
+ "#d9ead3",
615
+ "#cfe2f3",
616
+ "#f4cccc",
617
+ "#ead1dc",
618
+ "#d0e0e3",
619
+ ].map((hex) => (_jsx("button", { title: hex, style: {
620
+ width: 22,
621
+ height: 22,
622
+ background: hex,
623
+ border: "1px solid #ccc",
624
+ }, onClick: () => applyBackground(hex) }, hex))) }), _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsx("button", { onClick: toggleBorder, children: "Toggle border" }), _jsx("button", { onClick: mergeSelected, disabled: !normSel, children: "Merge selected" }), _jsx("button", { onClick: splitCell, children: "Split cell" }), _jsx("hr", {}), _jsx("button", { onClick: insertRowAbove, children: "Insert row above" }), _jsx("button", { onClick: insertRowBelow, children: "Insert row below" }), _jsx("button", { onClick: insertColLeft, children: "Insert column left" }), _jsx("button", { onClick: insertColRight, children: "Insert column right" }), _jsx("button", { onClick: deleteRow, children: "Delete row" }), _jsx("button", { onClick: deleteCol, children: "Delete column" }), _jsx("hr", {}), _jsx("button", { onClick: toggleFreezeHeader, children: tableNode.freeze_header
625
+ ? "Unfreeze header row"
626
+ : "Freeze header row" }), _jsx("button", { onClick: toggleFreezeFirstCol, children: tableNode.freeze_first_col
627
+ ? "Unfreeze first column"
628
+ : "Freeze first column" })] })] }))] }));
629
+ }
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ type Props = {
3
+ x: number;
4
+ y: number;
5
+ onClose: () => void;
6
+ onBackground: (color: string) => void;
7
+ onMerge: () => void;
8
+ onSplit: () => void;
9
+ };
10
+ export declare const TableContextMenu: React.FC<Props>;
11
+ export {};
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export const TableContextMenu = ({ x, y, onClose, onBackground, onMerge, onSplit, }) => {
3
+ return (_jsxs("div", { style: {
4
+ position: "fixed",
5
+ top: y,
6
+ left: x,
7
+ background: "#fff",
8
+ border: "1px solid #ddd",
9
+ borderRadius: 4,
10
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
11
+ zIndex: 2000,
12
+ padding: 8,
13
+ minWidth: 160,
14
+ }, onMouseLeave: onClose, children: [_jsx("button", { onClick: () => onBackground("#f9f871"), children: "Highlight Yellow" }), _jsx("button", { onClick: () => onBackground("#d7f7d7"), children: "Highlight Green" }), _jsx("button", { onClick: () => onMerge(), children: "Merge Cells" }), _jsx("button", { onClick: () => onSplit(), children: "Split Cell" })] }));
15
+ };
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ interface TableInsertDialogProps {
3
+ onInsert: (rows: number, cols: number) => void;
4
+ onClose: () => void;
5
+ }
6
+ export declare const TableInsertDialog: React.FC<TableInsertDialogProps>;
7
+ export {};