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,1066 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ export function ClassicEditor({ value, onChange, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, }) {
4
+ const editableRef = useRef(null);
5
+ const lastEmittedRef = useRef("");
6
+ const isComposingRef = useRef(false);
7
+ const fileInputRef = useRef(null);
8
+ const [selectedImage, setSelectedImage] = useState(null);
9
+ const [imageOverlay, setImageOverlay] = useState(null);
10
+ const resizingRef = useRef(null);
11
+ const [showTableDialog, setShowTableDialog] = useState(false);
12
+ const [tableRows, setTableRows] = useState(3);
13
+ const [tableCols, setTableCols] = useState(3);
14
+ const [tableMenu, setTableMenu] = useState(null);
15
+ const selectionRef = useRef(null);
16
+ const selectingRef = useRef(null);
17
+ useEffect(() => {
18
+ const el = editableRef.current;
19
+ if (!el)
20
+ return;
21
+ // Initialize with provided HTML only when externally controlled value changes
22
+ if (typeof value === "string" && value !== el.innerHTML) {
23
+ el.innerHTML = value || "";
24
+ }
25
+ // Suppress native context menu inside table cells at capture phase
26
+ const onCtx = (evt) => {
27
+ const target = evt.target;
28
+ const cell = getClosestCell(target);
29
+ if (cell) {
30
+ evt.preventDefault();
31
+ }
32
+ };
33
+ el.addEventListener("contextmenu", onCtx, { capture: true });
34
+ return () => {
35
+ el.removeEventListener("contextmenu", onCtx, { capture: true });
36
+ };
37
+ }, [value]);
38
+ const exec = (command, valueArg) => {
39
+ try {
40
+ document.execCommand(command, false, valueArg);
41
+ // Emit after command
42
+ const el = editableRef.current;
43
+ if (el && onChange) {
44
+ const html = el.innerHTML;
45
+ if (html !== lastEmittedRef.current) {
46
+ lastEmittedRef.current = html;
47
+ onChange(html);
48
+ }
49
+ }
50
+ }
51
+ catch { }
52
+ };
53
+ const applyFormatBlock = (blockName) => {
54
+ exec("formatBlock", blockName);
55
+ };
56
+ const insertLink = () => {
57
+ const url = window.prompt("Enter URL", "https://");
58
+ if (!url)
59
+ return;
60
+ exec("createLink", url);
61
+ };
62
+ const insertImage = () => {
63
+ fileInputRef.current?.click();
64
+ };
65
+ const scheduleImageOverlay = () => {
66
+ const img = selectedImage;
67
+ if (!img) {
68
+ setImageOverlay(null);
69
+ return;
70
+ }
71
+ try {
72
+ const rect = img.getBoundingClientRect();
73
+ setImageOverlay({
74
+ left: rect.left,
75
+ top: rect.top,
76
+ width: rect.width,
77
+ height: rect.height,
78
+ });
79
+ }
80
+ catch {
81
+ setImageOverlay(null);
82
+ }
83
+ };
84
+ useEffect(() => {
85
+ scheduleImageOverlay();
86
+ }, [selectedImage]);
87
+ useEffect(() => {
88
+ const onScroll = () => scheduleImageOverlay();
89
+ const onResize = () => scheduleImageOverlay();
90
+ window.addEventListener("scroll", onScroll, true);
91
+ window.addEventListener("resize", onResize);
92
+ return () => {
93
+ window.removeEventListener("scroll", onScroll, true);
94
+ window.removeEventListener("resize", onResize);
95
+ };
96
+ }, []);
97
+ const insertImageAtSelection = (src) => {
98
+ try {
99
+ const host = editableRef.current;
100
+ if (!host)
101
+ return;
102
+ host.focus();
103
+ let sel = window.getSelection();
104
+ let range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
105
+ if (!range || !host.contains(range.commonAncestorContainer)) {
106
+ range = document.createRange();
107
+ range.selectNodeContents(host);
108
+ range.collapse(false);
109
+ sel = window.getSelection();
110
+ sel?.removeAllRanges();
111
+ sel?.addRange(range);
112
+ }
113
+ const img = document.createElement("img");
114
+ img.src = src;
115
+ img.style.maxWidth = "100%";
116
+ img.style.height = "auto";
117
+ img.style.display = "inline-block";
118
+ img.alt = "image";
119
+ if (range) {
120
+ range.insertNode(img);
121
+ }
122
+ else {
123
+ host.appendChild(img);
124
+ }
125
+ const r = document.createRange();
126
+ r.setStartAfter(img);
127
+ r.collapse(true);
128
+ const s = window.getSelection();
129
+ s?.removeAllRanges();
130
+ s?.addRange(r);
131
+ setSelectedImage(img);
132
+ scheduleImageOverlay();
133
+ handleInput();
134
+ }
135
+ catch { }
136
+ };
137
+ const handleLocalImageFiles = async (files) => {
138
+ const list = Array.from(files).filter((f) => f.type.startsWith("image/"));
139
+ for (const f of list) {
140
+ await new Promise((resolve) => {
141
+ const reader = new FileReader();
142
+ reader.onload = () => {
143
+ const dataUrl = String(reader.result || "");
144
+ if (dataUrl)
145
+ insertImageAtSelection(dataUrl);
146
+ resolve();
147
+ };
148
+ reader.onerror = () => resolve();
149
+ reader.readAsDataURL(f);
150
+ });
151
+ }
152
+ };
153
+ const handleInput = () => {
154
+ if (isComposingRef.current)
155
+ return;
156
+ const el = editableRef.current;
157
+ if (!el || !onChange)
158
+ return;
159
+ const html = el.innerHTML;
160
+ if (html !== lastEmittedRef.current) {
161
+ lastEmittedRef.current = html;
162
+ onChange(html);
163
+ }
164
+ };
165
+ const buildTableHTML = (rows, cols) => {
166
+ const safeRows = Math.max(1, Math.min(50, Math.floor(rows) || 1));
167
+ const safeCols = Math.max(1, Math.min(20, Math.floor(cols) || 1));
168
+ let html = '<table style="border-collapse:collapse;width:100%;"><tbody>';
169
+ for (let r = 0; r < safeRows; r++) {
170
+ html += "<tr>";
171
+ for (let c = 0; c < safeCols; c++) {
172
+ html +=
173
+ '<td style="border:1px solid #ddd;padding:6px;min-width:60px;">&nbsp;</td>';
174
+ }
175
+ html += "</tr>";
176
+ }
177
+ html += "</tbody></table>";
178
+ return html;
179
+ };
180
+ const insertTable = () => {
181
+ try {
182
+ const el = editableRef.current;
183
+ if (!el)
184
+ return;
185
+ // Ensure editor is focused and selection is inside
186
+ el.focus();
187
+ let sel = window.getSelection();
188
+ let range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
189
+ if (!range || !el.contains(range.commonAncestorContainer)) {
190
+ // Place caret at end of editor
191
+ range = document.createRange();
192
+ range.selectNodeContents(el);
193
+ range.collapse(false);
194
+ sel = window.getSelection();
195
+ sel?.removeAllRanges();
196
+ sel?.addRange(range);
197
+ }
198
+ const html = buildTableHTML(tableRows, tableCols);
199
+ // Insert via Range for broader support
200
+ const wrapper = document.createElement("div");
201
+ wrapper.innerHTML = html;
202
+ const node = wrapper.firstChild;
203
+ if (!node || !range)
204
+ return;
205
+ range.insertNode(node);
206
+ // Move caret into first cell
207
+ const firstCell = node.querySelector("td,th");
208
+ if (firstCell)
209
+ moveCaretToCell(firstCell, false);
210
+ handleInput();
211
+ }
212
+ catch { }
213
+ };
214
+ const getClosestCell = (node) => {
215
+ let el = node;
216
+ while (el && el !== editableRef.current) {
217
+ if (el.nodeName === "TD" || el.nodeName === "TH") {
218
+ return el;
219
+ }
220
+ el = el.parentElement;
221
+ }
222
+ return null;
223
+ };
224
+ const moveCaretToCell = (cell, atEnd) => {
225
+ try {
226
+ const range = document.createRange();
227
+ // Ensure the cell has at least one text node
228
+ if (!cell.firstChild) {
229
+ const text = document.createTextNode("\u00A0");
230
+ cell.appendChild(text);
231
+ }
232
+ const textNode = cell.firstChild;
233
+ const len = (textNode.textContent || "").length;
234
+ range.setStart(textNode, atEnd ? len : 0);
235
+ range.collapse(true);
236
+ const sel = window.getSelection();
237
+ sel?.removeAllRanges();
238
+ sel?.addRange(range);
239
+ }
240
+ catch { }
241
+ };
242
+ const getCellPosition = (cell) => {
243
+ const row = cell.parentElement;
244
+ const tbody = row?.parentElement;
245
+ const table = tbody?.parentElement;
246
+ if (!row || !tbody || !table)
247
+ return null;
248
+ const rows = Array.from(tbody.querySelectorAll("tr"));
249
+ const rIdx = rows.indexOf(row);
250
+ const cells = Array.from(row.children).filter((c) => ["TD", "TH"].includes(c.tagName));
251
+ const cIdx = cells.indexOf(cell);
252
+ return { row, tbody, table, rIdx, cIdx };
253
+ };
254
+ const cellsOfRow = (row) => Array.from(row.children).filter((c) => ["TD", "TH"].includes(c.tagName));
255
+ const clearSelectionDecor = () => {
256
+ const sel = selectionRef.current;
257
+ if (!sel)
258
+ return;
259
+ const { tbody, sr, sc, er, ec } = sel;
260
+ const rows = Array.from(tbody.querySelectorAll("tr"));
261
+ for (let r = sr; r <= er; r++) {
262
+ const row = rows[r];
263
+ const cells = cellsOfRow(row);
264
+ for (let c = sc; c <= ec; c++) {
265
+ const cell = cells[c];
266
+ if (!cell)
267
+ continue;
268
+ if (cell.__rtePrevBg != null) {
269
+ cell.style.background = cell.__rtePrevBg;
270
+ delete cell.__rtePrevBg;
271
+ }
272
+ cell.style.outline = "";
273
+ cell.style.outlineOffset = "";
274
+ }
275
+ }
276
+ selectionRef.current = null;
277
+ };
278
+ const updateSelectionDecor = (tbody, sr, sc, er, ec) => {
279
+ clearSelectionDecor();
280
+ selectionRef.current = { tbody, sr, sc, er, ec };
281
+ const rows = Array.from(tbody.querySelectorAll("tr"));
282
+ for (let r = sr; r <= er; r++) {
283
+ const row = rows[r];
284
+ const cells = cellsOfRow(row);
285
+ for (let c = sc; c <= ec; c++) {
286
+ const cell = cells[c];
287
+ if (!cell)
288
+ continue;
289
+ cell.__rtePrevBg =
290
+ cell.style.background || "";
291
+ cell.style.background = "rgba(30,144,255,0.15)";
292
+ cell.style.outline = "2px solid #1e90ff";
293
+ cell.style.outlineOffset = "-2px";
294
+ }
295
+ }
296
+ };
297
+ const canMergeSelection = () => {
298
+ const sel = selectionRef.current;
299
+ if (!sel)
300
+ return false;
301
+ return sel.sr !== sel.er || sel.sc !== sel.ec;
302
+ };
303
+ const mergeSelection = () => {
304
+ const sel = selectionRef.current;
305
+ if (!sel)
306
+ return;
307
+ const { tbody, sr, sc, er, ec } = sel;
308
+ const rows = Array.from(tbody.querySelectorAll("tr"));
309
+ const anchorRow = rows[sr];
310
+ const anchor = cellsOfRow(anchorRow)[sc];
311
+ if (!anchor)
312
+ return;
313
+ // Collect content and remove other cells
314
+ const contents = [];
315
+ for (let r = sr; r <= er; r++) {
316
+ const row = rows[r];
317
+ const cells = cellsOfRow(row);
318
+ for (let c = sc; c <= ec; c++) {
319
+ const cell = cells[c];
320
+ if (!cell)
321
+ continue;
322
+ if (r === sr && c === sc)
323
+ continue;
324
+ const html = cell.innerHTML.trim();
325
+ if (html)
326
+ contents.push(html);
327
+ }
328
+ }
329
+ if (contents.length) {
330
+ anchor.innerHTML = (anchor.innerHTML || "") + " " + contents.join(" ");
331
+ }
332
+ // Set spans
333
+ anchor.colSpan = ec - sc + 1;
334
+ anchor.rowSpan = er - sr + 1;
335
+ // Remove other cells
336
+ for (let r = sr; r <= er; r++) {
337
+ const row = rows[r];
338
+ const cells = cellsOfRow(row);
339
+ for (let c = ec; c >= sc; c--) {
340
+ const cell = cells[c];
341
+ if (!cell)
342
+ continue;
343
+ if (r === sr && c === sc)
344
+ continue;
345
+ cell.remove();
346
+ }
347
+ }
348
+ moveCaretToCell(anchor, false);
349
+ clearSelectionDecor();
350
+ handleInput();
351
+ };
352
+ const addRow = (cell, dir) => {
353
+ const pos = getCellPosition(cell);
354
+ if (!pos)
355
+ return;
356
+ const { row, tbody, rIdx } = pos;
357
+ const newRow = document.createElement("tr");
358
+ const numCols = Array.from(row.children).filter((c) => ["TD", "TH"].includes(c.tagName)).length;
359
+ for (let i = 0; i < numCols; i++) {
360
+ const td = document.createElement("td");
361
+ td.style.border = "1px solid #ddd";
362
+ td.style.padding = "6px";
363
+ td.style.minWidth = "60px";
364
+ td.innerHTML = "&nbsp;";
365
+ newRow.appendChild(td);
366
+ }
367
+ const insertIndex = dir === "above" ? rIdx : rIdx + 1;
368
+ const refRow = tbody.children[insertIndex] || null;
369
+ tbody.insertBefore(newRow, refRow);
370
+ };
371
+ const deleteRow = (cell) => {
372
+ const pos = getCellPosition(cell);
373
+ if (!pos)
374
+ return;
375
+ const { row, tbody, table } = pos;
376
+ tbody.removeChild(row);
377
+ if (tbody.querySelectorAll("tr").length === 0) {
378
+ table.parentElement?.removeChild(table);
379
+ }
380
+ };
381
+ const addCol = (cell, dir) => {
382
+ const pos = getCellPosition(cell);
383
+ if (!pos)
384
+ return;
385
+ const { tbody, cIdx } = pos;
386
+ const rows = Array.from(tbody.querySelectorAll("tr"));
387
+ const insertIndex = dir === "left" ? cIdx : cIdx + 1;
388
+ for (const r of rows) {
389
+ const cells = Array.from(r.children).filter((c) => ["TD", "TH"].includes(c.tagName));
390
+ const td = document.createElement("td");
391
+ td.style.border = "1px solid #ddd";
392
+ td.style.padding = "6px";
393
+ td.style.minWidth = "60px";
394
+ td.innerHTML = "&nbsp;";
395
+ const ref = cells[insertIndex] || null;
396
+ if (ref)
397
+ r.insertBefore(td, ref);
398
+ else
399
+ r.appendChild(td);
400
+ }
401
+ };
402
+ const deleteCol = (cell) => {
403
+ const pos = getCellPosition(cell);
404
+ if (!pos)
405
+ return;
406
+ const { tbody, table, cIdx } = pos;
407
+ const rows = Array.from(tbody.querySelectorAll("tr"));
408
+ for (const r of rows) {
409
+ const cells = Array.from(r.children).filter((c) => ["TD", "TH"].includes(c.tagName));
410
+ const target = cells[cIdx];
411
+ if (target)
412
+ r.removeChild(target);
413
+ }
414
+ // If table has no columns left, remove it
415
+ const hasAnyCell = table.querySelector("td,th");
416
+ if (!hasAnyCell)
417
+ table.parentElement?.removeChild(table);
418
+ };
419
+ const toggleHeaderCell = (cell) => {
420
+ const isTh = cell.tagName === "TH";
421
+ const replacement = document.createElement(isTh ? "td" : "th");
422
+ replacement.innerHTML = cell.innerHTML || "&nbsp;";
423
+ replacement.style.border =
424
+ cell.style.border || "1px solid #ddd";
425
+ replacement.style.padding = cell.style.padding || "6px";
426
+ replacement.style.minWidth = cell.style.minWidth || "60px";
427
+ cell.parentElement?.replaceChild(replacement, cell);
428
+ };
429
+ const deleteTable = (cell) => {
430
+ const pos = getCellPosition(cell);
431
+ if (!pos)
432
+ return;
433
+ const { table } = pos;
434
+ table.parentElement?.removeChild(table);
435
+ };
436
+ const splitCell = (cell) => {
437
+ const pos = getCellPosition(cell);
438
+ if (!pos)
439
+ return;
440
+ const { tbody, rIdx, cIdx } = pos;
441
+ const rs = Math.max(1, cell.rowSpan || 1);
442
+ const cs = Math.max(1, cell.colSpan || 1);
443
+ if (rs === 1 && cs === 1)
444
+ return;
445
+ // Reset current cell
446
+ cell.rowSpan = 1;
447
+ cell.colSpan = 1;
448
+ // Add missing cells in the current row
449
+ const currentRow = Array.from(tbody.querySelectorAll("tr"))[rIdx];
450
+ for (let j = 1; j < cs; j++) {
451
+ const td = document.createElement("td");
452
+ td.style.border = "1px solid #ddd";
453
+ td.style.padding = "6px";
454
+ td.style.minWidth = "60px";
455
+ td.innerHTML = "&nbsp;";
456
+ const cells = cellsOfRow(currentRow);
457
+ const ref = cells[cIdx + j] || null;
458
+ currentRow.insertBefore(td, ref);
459
+ }
460
+ // For extra rows, insert cells at the same column index
461
+ for (let i = 1; i < rs; i++) {
462
+ const row = Array.from(tbody.querySelectorAll("tr"))[rIdx + i];
463
+ for (let j = 0; j < cs; j++) {
464
+ const td = document.createElement("td");
465
+ td.style.border = "1px solid #ddd";
466
+ td.style.padding = "6px";
467
+ td.style.minWidth = "60px";
468
+ td.innerHTML = "&nbsp;";
469
+ const cells = cellsOfRow(row);
470
+ const ref = cells[cIdx + j] || null;
471
+ row.insertBefore(td, ref);
472
+ }
473
+ }
474
+ handleInput();
475
+ };
476
+ const toggleHeaderRow = (cell) => {
477
+ const pos = getCellPosition(cell);
478
+ if (!pos)
479
+ return;
480
+ const { tbody } = pos;
481
+ const firstRow = tbody.querySelector("tr");
482
+ if (!firstRow)
483
+ return;
484
+ const cells = cellsOfRow(firstRow);
485
+ const shouldMakeHeader = cells.some((c) => c.tagName !== "TH");
486
+ for (const c of cells) {
487
+ const isTh = c.tagName === "TH";
488
+ if (shouldMakeHeader && !isTh) {
489
+ const th = document.createElement("th");
490
+ th.innerHTML = c.innerHTML || "&nbsp;";
491
+ th.style.border = c.style.border || "1px solid #ddd";
492
+ th.style.padding = c.style.padding || "6px";
493
+ th.style.minWidth = c.style.minWidth || "60px";
494
+ firstRow.replaceChild(th, c);
495
+ }
496
+ else if (!shouldMakeHeader && isTh) {
497
+ const td = document.createElement("td");
498
+ td.innerHTML = c.innerHTML || "&nbsp;";
499
+ td.style.border = c.style.border || "1px solid #ddd";
500
+ td.style.padding = c.style.padding || "6px";
501
+ td.style.minWidth = c.style.minWidth || "60px";
502
+ firstRow.replaceChild(td, c);
503
+ }
504
+ }
505
+ };
506
+ const applyBgToSelection = (hex, fallbackCell) => {
507
+ const sel = selectionRef.current;
508
+ if (sel) {
509
+ const rows = Array.from(sel.tbody.querySelectorAll("tr"));
510
+ for (let r = sel.sr; r <= sel.er; r++) {
511
+ const row = rows[r];
512
+ const cells = cellsOfRow(row);
513
+ for (let c = sel.sc; c <= sel.ec; c++) {
514
+ const cell = cells[c];
515
+ if (cell)
516
+ cell.style.background = hex;
517
+ }
518
+ }
519
+ }
520
+ else if (fallbackCell) {
521
+ fallbackCell.style.background = hex;
522
+ }
523
+ };
524
+ const toggleBorderSelection = (fallbackCell) => {
525
+ const applyToggle = (cell) => {
526
+ const cur = cell.style.border;
527
+ cell.style.border =
528
+ cur && cur !== "none" ? "none" : "1px solid #000";
529
+ };
530
+ const sel = selectionRef.current;
531
+ if (sel) {
532
+ const rows = Array.from(sel.tbody.querySelectorAll("tr"));
533
+ for (let r = sel.sr; r <= sel.er; r++) {
534
+ const row = rows[r];
535
+ const cells = cellsOfRow(row);
536
+ for (let c = sel.sc; c <= sel.ec; c++) {
537
+ const cell = cells[c];
538
+ if (cell)
539
+ applyToggle(cell);
540
+ }
541
+ }
542
+ }
543
+ else if (fallbackCell) {
544
+ applyToggle(fallbackCell);
545
+ }
546
+ };
547
+ return (_jsxs("div", { style: { border: "1px solid #ddd", borderRadius: 6 }, children: [_jsxs("div", { style: {
548
+ display: "flex",
549
+ flexWrap: "wrap",
550
+ gap: 8,
551
+ padding: 8,
552
+ borderBottom: "1px solid #eee",
553
+ background: "#fafafa",
554
+ position: "sticky",
555
+ top: 0,
556
+ zIndex: 1,
557
+ }, children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: "none" }, onChange: (e) => {
558
+ if (e.currentTarget.files)
559
+ handleLocalImageFiles(e.currentTarget.files);
560
+ e.currentTarget.value = "";
561
+ } }), _jsxs("select", { defaultValue: "p", onChange: (e) => {
562
+ const val = e.target.value;
563
+ if (val === "p")
564
+ applyFormatBlock("<p>");
565
+ else if (val === "h1")
566
+ applyFormatBlock("<h1>");
567
+ else if (val === "h2")
568
+ applyFormatBlock("<h2>");
569
+ else if (val === "h3")
570
+ applyFormatBlock("<h3>");
571
+ }, title: "Paragraph/Heading", children: [_jsx("option", { value: "p", children: "Paragraph" }), _jsx("option", { value: "h1", children: "Heading 1" }), _jsx("option", { value: "h2", children: "Heading 2" }), _jsx("option", { value: "h3", children: "Heading 3" })] }), _jsx("button", { onClick: () => exec("bold"), children: "B" }), _jsx("button", { onClick: () => exec("italic"), children: "I" }), _jsx("button", { onClick: () => exec("underline"), children: "U" }), _jsx("button", { onClick: () => exec("strikeThrough"), children: "S" }), _jsx("button", { onClick: () => exec("insertUnorderedList"), children: "\u2022 List" }), _jsx("button", { onClick: () => exec("insertOrderedList"), children: "1. List" }), _jsx("button", { onClick: () => exec("formatBlock", "<blockquote>"), children: "\u275D" }), _jsx("button", { onClick: () => exec("formatBlock", "<pre>"), children: "< />" }), _jsx("button", { onClick: insertLink, children: "Link" }), _jsx("button", { onClick: () => exec("unlink"), children: "Unlink" }), _jsx("button", { onClick: insertImage, children: "Image" }), _jsxs("div", { style: {
572
+ display: "inline-flex",
573
+ gap: 4,
574
+ alignItems: "center",
575
+ marginLeft: 6,
576
+ }, children: [_jsx("span", { style: { fontSize: 12, opacity: 0.7 }, children: "Image align:" }), _jsx("button", { onClick: () => {
577
+ const img = selectedImage;
578
+ if (!img)
579
+ return;
580
+ img.style.display = "block";
581
+ img.style.margin = "0 auto";
582
+ img.style.float = "none";
583
+ scheduleImageOverlay();
584
+ handleInput();
585
+ }, title: "Center", children: "\u2299" }), _jsx("button", { onClick: () => {
586
+ const img = selectedImage;
587
+ if (!img)
588
+ return;
589
+ img.style.display = "inline";
590
+ img.style.float = "left";
591
+ img.style.margin = "0 8px 8px 0";
592
+ scheduleImageOverlay();
593
+ handleInput();
594
+ }, title: "Float left", children: "\u27F8" }), _jsx("button", { onClick: () => {
595
+ const img = selectedImage;
596
+ if (!img)
597
+ return;
598
+ img.style.display = "inline";
599
+ img.style.float = "right";
600
+ img.style.margin = "0 0 8px 8px";
601
+ scheduleImageOverlay();
602
+ handleInput();
603
+ }, title: "Float right", children: "\u27F9" })] }), _jsx("button", { onClick: () => setShowTableDialog(true), children: "+ Table" }), _jsx("button", { onClick: () => exec("undo"), children: "Undo" }), _jsx("button", { onClick: () => exec("redo"), children: "Redo" })] }), showTableDialog && (_jsx("div", { style: {
604
+ position: "fixed",
605
+ inset: 0,
606
+ background: "rgba(0,0,0,0.35)",
607
+ display: "flex",
608
+ alignItems: "center",
609
+ justifyContent: "center",
610
+ zIndex: 50,
611
+ }, onClick: () => setShowTableDialog(false), children: _jsxs("div", { style: {
612
+ background: "#fff",
613
+ padding: 16,
614
+ borderRadius: 8,
615
+ minWidth: 280,
616
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 8 }, children: "Insert table" }), _jsxs("div", { style: { display: "flex", gap: 12, alignItems: "center" }, children: [_jsx("div", { style: {
617
+ display: "grid",
618
+ gridTemplateColumns: "repeat(10, 18px)",
619
+ gap: 2,
620
+ padding: 6,
621
+ border: "1px solid #eee",
622
+ }, children: Array.from({ length: 100 }).map((_, i) => {
623
+ const r = Math.floor(i / 10) + 1;
624
+ const c = (i % 10) + 1;
625
+ const active = r <= tableRows && c <= tableCols;
626
+ return (_jsx("div", { onMouseEnter: () => {
627
+ setTableRows(r);
628
+ setTableCols(c);
629
+ }, onClick: () => {
630
+ insertTable();
631
+ setShowTableDialog(false);
632
+ }, style: {
633
+ width: 16,
634
+ height: 16,
635
+ border: "1px solid #ccc",
636
+ background: active ? "#1e90ff" : "#fff",
637
+ } }, i));
638
+ }) }), _jsxs("div", { style: { fontSize: 12, minWidth: 48 }, children: [tableRows, " \u00D7 ", tableCols] })] }), _jsxs("div", { style: {
639
+ display: "flex",
640
+ gap: 8,
641
+ justifyContent: "end",
642
+ marginTop: 12,
643
+ }, children: [_jsx("button", { onClick: () => setShowTableDialog(false), children: "Cancel" }), _jsx("button", { onClick: () => {
644
+ insertTable();
645
+ setShowTableDialog(false);
646
+ }, children: "Insert" })] })] }) })), _jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onInput: handleInput, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
647
+ isComposingRef.current = false;
648
+ handleInput();
649
+ }, onPaste: (e) => {
650
+ const items = e.clipboardData?.files;
651
+ if (items && items.length) {
652
+ const hasImage = Array.from(items).some((f) => f.type.startsWith("image/"));
653
+ if (hasImage) {
654
+ e.preventDefault();
655
+ handleLocalImageFiles(items);
656
+ }
657
+ }
658
+ }, onDragOver: (e) => {
659
+ if (e.dataTransfer?.types?.includes("Files")) {
660
+ e.preventDefault();
661
+ }
662
+ }, onDrop: (e) => {
663
+ if (e.dataTransfer?.files?.length) {
664
+ e.preventDefault();
665
+ // Try to move caret to drop point
666
+ const x = e.clientX;
667
+ const y = e.clientY;
668
+ let range = null;
669
+ // @ts-ignore
670
+ if (document.caretRangeFromPoint) {
671
+ // @ts-ignore
672
+ range = document.caretRangeFromPoint(x, y);
673
+ }
674
+ else if (document.caretPositionFromPoint) {
675
+ const pos = document.caretPositionFromPoint(x, y);
676
+ if (pos) {
677
+ range = document.createRange();
678
+ range.setStart(pos.offsetNode, pos.offset);
679
+ }
680
+ }
681
+ if (range) {
682
+ const sel = window.getSelection();
683
+ sel?.removeAllRanges();
684
+ sel?.addRange(range);
685
+ }
686
+ handleLocalImageFiles(e.dataTransfer.files);
687
+ }
688
+ }, onClick: (e) => {
689
+ const t = e.target;
690
+ if (t && t.tagName === "IMG") {
691
+ setSelectedImage(t);
692
+ scheduleImageOverlay();
693
+ }
694
+ else {
695
+ setSelectedImage(null);
696
+ setImageOverlay(null);
697
+ }
698
+ }, style: {
699
+ padding: 12,
700
+ minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
701
+ maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
702
+ overflowY: "auto",
703
+ outline: "none",
704
+ lineHeight: 1.6,
705
+ }, "data-placeholder": placeholder, onFocus: (e) => {
706
+ // Ensure the editor has at least one paragraph to type into
707
+ const el = e.currentTarget;
708
+ if (!el.innerHTML || el.innerHTML === "<br>") {
709
+ el.innerHTML = "<p><br></p>";
710
+ }
711
+ }, onKeyDown: (e) => {
712
+ // Keep Tab for indentation in lists; otherwise insert 2 spaces
713
+ if (e.key === "Tab") {
714
+ e.preventDefault();
715
+ if (document.queryCommandState("insertUnorderedList") ||
716
+ document.queryCommandState("insertOrderedList")) {
717
+ exec(e.shiftKey ? "outdent" : "indent");
718
+ }
719
+ else {
720
+ document.execCommand("insertText", false, " ");
721
+ }
722
+ }
723
+ // Table navigation with arrows inside cells
724
+ if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
725
+ const sel = window.getSelection();
726
+ const cell = getClosestCell(sel?.anchorNode || null);
727
+ if (cell &&
728
+ cell.parentElement &&
729
+ cell.parentElement.parentElement) {
730
+ const row = cell.parentElement;
731
+ const tbody = row.parentElement;
732
+ const cells = Array.from(row.children).filter((c) => c.tagName === "TD" ||
733
+ c.tagName === "TH");
734
+ const rows = Array.from(tbody.children);
735
+ const rIdx = rows.indexOf(row);
736
+ const cIdx = cells.indexOf(cell);
737
+ const atStart = (sel?.anchorOffset || 0) === 0;
738
+ const cellTextLen = (cell.textContent || "").length;
739
+ const atEnd = (sel?.anchorOffset || 0) >= cellTextLen;
740
+ let target = null;
741
+ if (e.key === "ArrowLeft" && atStart && cIdx > 0) {
742
+ target = row.children[cIdx - 1];
743
+ }
744
+ else if (e.key === "ArrowRight" &&
745
+ atEnd &&
746
+ cIdx < row.children.length - 1) {
747
+ target = row.children[cIdx + 1];
748
+ }
749
+ else if (e.key === "ArrowUp" && rIdx > 0 && atStart) {
750
+ target = rows[rIdx - 1].children[cIdx];
751
+ }
752
+ else if (e.key === "ArrowDown" &&
753
+ rIdx < rows.length - 1 &&
754
+ atEnd) {
755
+ target = rows[rIdx + 1].children[cIdx];
756
+ }
757
+ if (target) {
758
+ e.preventDefault();
759
+ moveCaretToCell(target, e.key === "ArrowRight" || e.key === "ArrowDown");
760
+ }
761
+ }
762
+ }
763
+ }, onMouseDown: (e) => {
764
+ const cell = getClosestCell(e.target);
765
+ if (!cell) {
766
+ clearSelectionDecor();
767
+ return;
768
+ }
769
+ const pos = getCellPosition(cell);
770
+ if (!pos)
771
+ return;
772
+ selectingRef.current = { tbody: pos.tbody, start: cell };
773
+ const onMove = (ev) => {
774
+ const under = document.elementFromPoint(ev.clientX, ev.clientY);
775
+ const overCell = getClosestCell(under);
776
+ const startInfo = selectingRef.current;
777
+ if (!overCell || !startInfo)
778
+ return;
779
+ const a = getCellPosition(startInfo.start);
780
+ const b = getCellPosition(overCell);
781
+ if (!a || !b || a.tbody !== b.tbody)
782
+ return;
783
+ const sr = Math.min(a.rIdx, b.rIdx);
784
+ const sc = Math.min(a.cIdx, b.cIdx);
785
+ const er = Math.max(a.rIdx, b.rIdx);
786
+ const ec = Math.max(a.cIdx, b.cIdx);
787
+ updateSelectionDecor(a.tbody, sr, sc, er, ec);
788
+ };
789
+ const onUp = () => {
790
+ window.removeEventListener("mousemove", onMove);
791
+ window.removeEventListener("mouseup", onUp);
792
+ selectingRef.current = null;
793
+ };
794
+ window.addEventListener("mousemove", onMove);
795
+ window.addEventListener("mouseup", onUp);
796
+ }, onContextMenu: (e) => {
797
+ const cell = getClosestCell(e.target);
798
+ if (cell) {
799
+ e.preventDefault();
800
+ const vw = window.innerWidth;
801
+ const vh = window.innerHeight;
802
+ const menuW = 220;
803
+ const menuH = 300;
804
+ const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
805
+ const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
806
+ setTableMenu({ x, y, cell });
807
+ }
808
+ else {
809
+ setTableMenu(null);
810
+ }
811
+ }, dangerouslySetInnerHTML: { __html: value || "" } }), selectedImage && imageOverlay && (_jsxs("div", { style: {
812
+ position: "fixed",
813
+ left: imageOverlay.left,
814
+ top: imageOverlay.top,
815
+ width: imageOverlay.width,
816
+ height: imageOverlay.height,
817
+ pointerEvents: "none",
818
+ zIndex: 55,
819
+ }, children: [_jsx("div", { style: {
820
+ position: "absolute",
821
+ inset: 0,
822
+ outline: "2px solid #1e90ff",
823
+ outlineOffset: -2,
824
+ } }), _jsx("div", { title: "Resize", onMouseDown: (e) => {
825
+ e.preventDefault();
826
+ if (!selectedImage)
827
+ return;
828
+ resizingRef.current = {
829
+ side: "left",
830
+ startX: e.clientX,
831
+ startWidth: selectedImage.getBoundingClientRect().width,
832
+ };
833
+ const onMove = (ev) => {
834
+ const info = resizingRef.current;
835
+ if (!info || !selectedImage)
836
+ return;
837
+ const delta = info.startX - ev.clientX;
838
+ const next = Math.max(80, Math.round(info.startWidth + delta));
839
+ selectedImage.style.width = next + "px";
840
+ selectedImage.style.height = "auto";
841
+ scheduleImageOverlay();
842
+ };
843
+ const onUp = () => {
844
+ window.removeEventListener("mousemove", onMove);
845
+ window.removeEventListener("mouseup", onUp);
846
+ resizingRef.current = null;
847
+ handleInput();
848
+ };
849
+ window.addEventListener("mousemove", onMove);
850
+ window.addEventListener("mouseup", onUp);
851
+ }, style: {
852
+ position: "absolute",
853
+ left: -6,
854
+ top: "50%",
855
+ transform: "translateY(-50%)",
856
+ width: 8,
857
+ height: 24,
858
+ background: "#1e90ff",
859
+ borderRadius: 2,
860
+ cursor: "ew-resize",
861
+ pointerEvents: "auto",
862
+ } }), _jsx("div", { title: "Resize", onMouseDown: (e) => {
863
+ e.preventDefault();
864
+ if (!selectedImage)
865
+ return;
866
+ resizingRef.current = {
867
+ side: "right",
868
+ startX: e.clientX,
869
+ startWidth: selectedImage.getBoundingClientRect().width,
870
+ };
871
+ const onMove = (ev) => {
872
+ const info = resizingRef.current;
873
+ if (!info || !selectedImage)
874
+ return;
875
+ const delta = ev.clientX - info.startX;
876
+ const next = Math.max(80, Math.round(info.startWidth + delta));
877
+ selectedImage.style.width = next + "px";
878
+ selectedImage.style.height = "auto";
879
+ scheduleImageOverlay();
880
+ };
881
+ const onUp = () => {
882
+ window.removeEventListener("mousemove", onMove);
883
+ window.removeEventListener("mouseup", onUp);
884
+ resizingRef.current = null;
885
+ handleInput();
886
+ };
887
+ window.addEventListener("mousemove", onMove);
888
+ window.addEventListener("mouseup", onUp);
889
+ }, style: {
890
+ position: "absolute",
891
+ right: -6,
892
+ top: "50%",
893
+ transform: "translateY(-50%)",
894
+ width: 8,
895
+ height: 24,
896
+ background: "#1e90ff",
897
+ borderRadius: 2,
898
+ cursor: "ew-resize",
899
+ pointerEvents: "auto",
900
+ } })] })), tableMenu && (_jsx("div", { style: {
901
+ position: "fixed",
902
+ inset: 0,
903
+ zIndex: 60,
904
+ }, onClick: () => setTableMenu(null), onContextMenu: (e) => {
905
+ // Prevent native menu while overlay is shown and reposition our menu
906
+ e.preventDefault();
907
+ const vw = window.innerWidth;
908
+ const vh = window.innerHeight;
909
+ const menuW = 220;
910
+ const menuH = 300;
911
+ const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
912
+ const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
913
+ // Temporarily hide overlay to detect underlying cell
914
+ const overlay = e.currentTarget;
915
+ const prev = overlay.style.display;
916
+ overlay.style.display = "none";
917
+ const under = document.elementFromPoint(e.clientX, e.clientY);
918
+ overlay.style.display = prev;
919
+ const cell = getClosestCell(under);
920
+ if (cell)
921
+ setTableMenu({ x, y, cell });
922
+ }, children: _jsxs("div", { style: {
923
+ position: "fixed",
924
+ left: tableMenu.x,
925
+ top: tableMenu.y,
926
+ background: "#fff",
927
+ border: "1px solid #ddd",
928
+ borderRadius: 8,
929
+ boxShadow: "0 8px 24px rgba(0,0,0,0.18)",
930
+ padding: 6,
931
+ width: 200,
932
+ maxHeight: 260,
933
+ overflowY: "auto",
934
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, fontSize: 11, margin: "2px 6px 6px" }, children: "Table" }), _jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsxs("button", { style: {
935
+ display: "flex",
936
+ alignItems: "center",
937
+ gap: 8,
938
+ padding: "6px 8px",
939
+ fontSize: 12,
940
+ }, onClick: () => setShowTableDialog(true), children: [_jsx("span", { children: "\u2795" }), _jsx("span", { children: "Insert table\u2026" })] }), _jsx("hr", { style: { margin: "4px 0" } }), _jsxs("div", { style: {
941
+ display: "flex",
942
+ gap: 8,
943
+ alignItems: "center",
944
+ padding: "4px 6px",
945
+ fontSize: 12,
946
+ }, children: [_jsx("span", { children: "Fill:" }), _jsx("input", { type: "color", defaultValue: "#ffffff", onChange: (e) => {
947
+ applyBgToSelection(e.target.value, tableMenu.cell);
948
+ setTableMenu(null);
949
+ }, style: {
950
+ width: 28,
951
+ height: 18,
952
+ padding: 0,
953
+ border: "none",
954
+ background: "transparent",
955
+ } })] }), _jsxs("button", { style: {
956
+ display: "flex",
957
+ alignItems: "center",
958
+ gap: 8,
959
+ padding: "6px 8px",
960
+ fontSize: 12,
961
+ }, onClick: () => {
962
+ toggleBorderSelection(tableMenu.cell);
963
+ setTableMenu(null);
964
+ }, children: [_jsx("span", { children: "\u25A6" }), _jsx("span", { children: "Toggle border" })] }), _jsx("hr", { style: { margin: "4px 0" } }), _jsxs("button", { disabled: !canMergeSelection(), style: {
965
+ display: "flex",
966
+ alignItems: "center",
967
+ gap: 8,
968
+ padding: "6px 8px",
969
+ fontSize: 12,
970
+ opacity: canMergeSelection() ? 1 : 0.5,
971
+ cursor: canMergeSelection() ? "pointer" : "default",
972
+ }, onClick: () => {
973
+ mergeSelection();
974
+ setTableMenu(null);
975
+ }, children: [_jsx("span", { children: "\u21C4" }), _jsx("span", { children: "Merge cells" })] }), _jsxs("button", { style: {
976
+ display: "flex",
977
+ alignItems: "center",
978
+ gap: 8,
979
+ padding: "6px 8px",
980
+ fontSize: 12,
981
+ }, onClick: () => {
982
+ splitCell(tableMenu.cell);
983
+ setTableMenu(null);
984
+ }, children: [_jsx("span", { children: "\u2922" }), _jsx("span", { children: "Split cell" })] }), _jsx("hr", { style: { margin: "4px 0" } }), _jsxs("button", { style: {
985
+ display: "flex",
986
+ alignItems: "center",
987
+ gap: 8,
988
+ padding: "6px 8px",
989
+ fontSize: 12,
990
+ }, onClick: () => {
991
+ addRow(tableMenu.cell, "above");
992
+ setTableMenu(null);
993
+ }, children: [_jsx("span", { children: "\u21A5" }), _jsx("span", { children: "Row above" })] }), _jsxs("button", { style: {
994
+ display: "flex",
995
+ alignItems: "center",
996
+ gap: 8,
997
+ padding: "6px 8px",
998
+ fontSize: 12,
999
+ }, onClick: () => {
1000
+ addRow(tableMenu.cell, "below");
1001
+ setTableMenu(null);
1002
+ }, children: [_jsx("span", { children: "\u21A7" }), _jsx("span", { children: "Row below" })] }), _jsxs("button", { style: {
1003
+ display: "flex",
1004
+ alignItems: "center",
1005
+ gap: 8,
1006
+ padding: "6px 8px",
1007
+ fontSize: 12,
1008
+ }, onClick: () => {
1009
+ addCol(tableMenu.cell, "left");
1010
+ setTableMenu(null);
1011
+ }, children: [_jsx("span", { children: "\u2190" }), _jsx("span", { children: "Column left" })] }), _jsxs("button", { style: {
1012
+ display: "flex",
1013
+ alignItems: "center",
1014
+ gap: 8,
1015
+ padding: "6px 8px",
1016
+ fontSize: 12,
1017
+ }, onClick: () => {
1018
+ addCol(tableMenu.cell, "right");
1019
+ setTableMenu(null);
1020
+ }, children: [_jsx("span", { children: "\u2192" }), _jsx("span", { children: "Column right" })] }), _jsxs("button", { style: {
1021
+ display: "flex",
1022
+ alignItems: "center",
1023
+ gap: 8,
1024
+ padding: "6px 8px",
1025
+ fontSize: 12,
1026
+ }, onClick: () => {
1027
+ deleteRow(tableMenu.cell);
1028
+ setTableMenu(null);
1029
+ }, children: [_jsx("span", { children: "\u2716" }), _jsx("span", { children: "Delete row" })] }), _jsxs("button", { style: {
1030
+ display: "flex",
1031
+ alignItems: "center",
1032
+ gap: 8,
1033
+ padding: "6px 8px",
1034
+ fontSize: 12,
1035
+ }, onClick: () => {
1036
+ deleteCol(tableMenu.cell);
1037
+ setTableMenu(null);
1038
+ }, children: [_jsx("span", { children: "\u2716" }), _jsx("span", { children: "Delete column" })] }), _jsxs("button", { style: {
1039
+ display: "flex",
1040
+ alignItems: "center",
1041
+ gap: 8,
1042
+ padding: "6px 8px",
1043
+ fontSize: 12,
1044
+ }, onClick: () => {
1045
+ toggleHeaderCell(tableMenu.cell);
1046
+ setTableMenu(null);
1047
+ }, children: [_jsx("span", { children: "H" }), _jsx("span", { children: "Toggle header" })] }), _jsxs("button", { style: {
1048
+ display: "flex",
1049
+ alignItems: "center",
1050
+ gap: 8,
1051
+ padding: "6px 8px",
1052
+ fontSize: 12,
1053
+ }, onClick: () => {
1054
+ toggleHeaderRow(tableMenu.cell);
1055
+ setTableMenu(null);
1056
+ }, children: [_jsx("span", { children: "H\u2081" }), _jsx("span", { children: "Toggle header row" })] }), _jsx("hr", { style: { margin: "4px 0" } }), _jsxs("button", { style: {
1057
+ display: "flex",
1058
+ alignItems: "center",
1059
+ gap: 8,
1060
+ padding: "6px 8px",
1061
+ fontSize: 12,
1062
+ }, onClick: () => {
1063
+ deleteTable(tableMenu.cell);
1064
+ setTableMenu(null);
1065
+ }, children: [_jsx("span", { children: "\uD83D\uDDD1" }), _jsx("span", { children: "Delete table" })] })] })] }) }))] }));
1066
+ }