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.
- package/dist/QuillEditor.d.ts +8 -0
- package/dist/QuillEditor.js +34 -0
- package/dist/app.d.ts +6 -0
- package/dist/app.js +6 -0
- package/dist/blots/CommentBlot.d.ts +8 -0
- package/dist/blots/CommentBlot.js +17 -0
- package/dist/blots/FormulaBlot.d.ts +12 -0
- package/dist/blots/FormulaBlot.js +36 -0
- package/dist/blots/MediaBlot.d.ts +11 -0
- package/dist/blots/MediaBlot.js +37 -0
- package/dist/blots/TableBlot.d.ts +10 -0
- package/dist/blots/TableBlot.js +54 -0
- package/dist/blots/index.d.ts +5 -0
- package/dist/blots/index.js +12 -0
- package/dist/components/ClassicEditor.d.ts +10 -0
- package/dist/components/ClassicEditor.js +1066 -0
- package/dist/components/DiagramEditor.d.ts +5 -0
- package/dist/components/DiagramEditor.js +73 -0
- package/dist/components/FormulaEditor.d.ts +6 -0
- package/dist/components/FormulaEditor.js +86 -0
- package/dist/components/InfoBox.d.ts +7 -0
- package/dist/components/InfoBox.js +18 -0
- package/dist/components/MCQBlock.d.ts +13 -0
- package/dist/components/MCQBlock.js +29 -0
- package/dist/components/SmartEditor.d.ts +0 -0
- package/dist/components/SmartEditor.js +1 -0
- package/dist/components/SmartTable.d.ts +22 -0
- package/dist/components/SmartTable.js +629 -0
- package/dist/components/TableContextMenu.d.ts +11 -0
- package/dist/components/TableContextMenu.js +15 -0
- package/dist/components/TableInsertDialog.d.ts +7 -0
- package/dist/components/TableInsertDialog.js +42 -0
- package/dist/hooks/useEditorSync.d.ts +5 -0
- package/dist/hooks/useEditorSync.js +53 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/smart-editor.d.ts +0 -0
- package/dist/smart-editor.js +1 -0
- package/dist/standalone/classic-editor-embed.d.ts +12 -0
- package/dist/standalone/classic-editor-embed.js +108 -0
- package/dist/standalone/editor.js +241 -0
- 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;"> </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 = " ";
|
|
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 = " ";
|
|
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 || " ";
|
|
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 = " ";
|
|
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 = " ";
|
|
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 || " ";
|
|
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 || " ";
|
|
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
|
+
}
|