smartrte-react 0.1.13 → 0.1.15
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/components/ClassicEditor.d.ts +16 -1
- package/dist/components/ClassicEditor.js +898 -289
- package/package.json +25 -20
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { MediaManager } from "./MediaManager";
|
|
4
|
-
|
|
4
|
+
import * as pdfjsLib from 'pdfjs-dist';
|
|
5
|
+
import mammoth from 'mammoth';
|
|
6
|
+
// Initialize PDF.js worker
|
|
7
|
+
if (typeof window !== 'undefined') {
|
|
8
|
+
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
|
9
|
+
}
|
|
10
|
+
export function ClassicEditor({ value, onChange, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, table = true, media = true, formula = true, mediaManager, fonts = [
|
|
11
|
+
{ name: "Arial", value: "Arial, Helvetica, sans-serif" },
|
|
12
|
+
{ name: "Georgia", value: "Georgia, serif" },
|
|
13
|
+
{ name: "Impact", value: "Impact, Charcoal, sans-serif" },
|
|
14
|
+
{ name: "Tahoma", value: "Tahoma, Geneva, sans-serif" },
|
|
15
|
+
{ name: "Times New Roman", value: "'Times New Roman', Times, serif" },
|
|
16
|
+
{ name: "Verdana", value: "Verdana, Geneva, sans-serif" },
|
|
17
|
+
{ name: "Courier New", value: "'Courier New', Courier, monospace" },
|
|
18
|
+
], defaultFont, }) {
|
|
5
19
|
const editableRef = useRef(null);
|
|
6
20
|
const lastEmittedRef = useRef("");
|
|
7
21
|
const isComposingRef = useRef(false);
|
|
8
22
|
const fileInputRef = useRef(null);
|
|
23
|
+
const pdfInputRef = useRef(null);
|
|
24
|
+
const docxInputRef = useRef(null);
|
|
25
|
+
const [loadingPdf, setLoadingPdf] = useState(false);
|
|
26
|
+
const [loadingDocx, setLoadingDocx] = useState(false);
|
|
27
|
+
// State for import confirmation
|
|
28
|
+
const [pendingImport, setPendingImport] = useState(null);
|
|
9
29
|
const replaceTargetRef = useRef(null);
|
|
10
30
|
const [selectedImage, setSelectedImage] = useState(null);
|
|
11
31
|
const [imageOverlay, setImageOverlay] = useState(null);
|
|
@@ -25,7 +45,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
25
45
|
const [showColorPicker, setShowColorPicker] = useState(false);
|
|
26
46
|
const [colorPickerType, setColorPickerType] = useState('text');
|
|
27
47
|
const savedRangeRef = useRef(null);
|
|
28
|
-
const [currentFontSize, setCurrentFontSize] = useState("
|
|
48
|
+
const [currentFontSize, setCurrentFontSize] = useState("");
|
|
49
|
+
const [currentFont, setCurrentFont] = useState("");
|
|
29
50
|
useEffect(() => {
|
|
30
51
|
const el = editableRef.current;
|
|
31
52
|
if (!el)
|
|
@@ -33,6 +54,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
33
54
|
// Initialize with provided HTML only when externally controlled value changes
|
|
34
55
|
if (typeof value === "string" && value !== el.innerHTML) {
|
|
35
56
|
el.innerHTML = value || "";
|
|
57
|
+
fixNegativeMargins(el);
|
|
58
|
+
ensureTableWrappers(el);
|
|
59
|
+
addTableResizeHandles();
|
|
36
60
|
}
|
|
37
61
|
// Suppress native context menu inside table cells at capture phase
|
|
38
62
|
const onCtx = (evt) => {
|
|
@@ -152,6 +176,57 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
152
176
|
console.error('Error applying font size:', error);
|
|
153
177
|
}
|
|
154
178
|
};
|
|
179
|
+
const applyFontFamily = (font) => {
|
|
180
|
+
try {
|
|
181
|
+
setCurrentFont(font);
|
|
182
|
+
const editor = editableRef.current;
|
|
183
|
+
if (!editor)
|
|
184
|
+
return;
|
|
185
|
+
editor.focus();
|
|
186
|
+
let range = null;
|
|
187
|
+
const sel = window.getSelection();
|
|
188
|
+
if (sel && sel.rangeCount > 0) {
|
|
189
|
+
const currentRange = sel.getRangeAt(0);
|
|
190
|
+
if (editor.contains(currentRange.commonAncestorContainer)) {
|
|
191
|
+
range = currentRange;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!range && savedRangeRef.current) {
|
|
195
|
+
range = savedRangeRef.current.cloneRange();
|
|
196
|
+
}
|
|
197
|
+
if (!range)
|
|
198
|
+
return;
|
|
199
|
+
if (range.collapsed) {
|
|
200
|
+
const span = document.createElement('span');
|
|
201
|
+
span.style.fontFamily = font;
|
|
202
|
+
span.textContent = '\u200B';
|
|
203
|
+
range.insertNode(span);
|
|
204
|
+
const newRange = document.createRange();
|
|
205
|
+
newRange.setStart(span.firstChild, 1);
|
|
206
|
+
newRange.collapse(true);
|
|
207
|
+
if (sel) {
|
|
208
|
+
sel.removeAllRanges();
|
|
209
|
+
sel.addRange(newRange);
|
|
210
|
+
}
|
|
211
|
+
handleInput();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const span = document.createElement('span');
|
|
215
|
+
span.style.fontFamily = font;
|
|
216
|
+
const fragment = range.extractContents();
|
|
217
|
+
span.appendChild(fragment);
|
|
218
|
+
range.insertNode(span);
|
|
219
|
+
if (sel) {
|
|
220
|
+
range.selectNodeContents(span);
|
|
221
|
+
sel.removeAllRanges();
|
|
222
|
+
sel.addRange(range);
|
|
223
|
+
}
|
|
224
|
+
handleInput();
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
console.error('Error applying font family:', error);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
155
230
|
const applyTextColor = (color) => {
|
|
156
231
|
exec("foreColor", color);
|
|
157
232
|
};
|
|
@@ -205,14 +280,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
205
280
|
if (!table)
|
|
206
281
|
return;
|
|
207
282
|
const target = e.target;
|
|
208
|
-
|
|
209
|
-
|
|
283
|
+
const cell = getClosestCell(target);
|
|
284
|
+
if (cell) {
|
|
285
|
+
const rect = cell.getBoundingClientRect();
|
|
210
286
|
const rightEdge = rect.right;
|
|
211
287
|
const clickX = e.clientX;
|
|
212
288
|
if (Math.abs(clickX - rightEdge) < 5) {
|
|
213
289
|
e.preventDefault();
|
|
214
|
-
const tableElem =
|
|
215
|
-
const colIndex = parseInt(
|
|
290
|
+
const tableElem = cell.closest('table');
|
|
291
|
+
const colIndex = parseInt(cell.getAttribute('data-col-index') || '0', 10);
|
|
216
292
|
if (tableElem) {
|
|
217
293
|
startColumnResize(tableElem, colIndex, e.clientX);
|
|
218
294
|
}
|
|
@@ -222,8 +298,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
222
298
|
const clickY = e.clientY;
|
|
223
299
|
if (Math.abs(clickY - bottomEdge) < 5) {
|
|
224
300
|
e.preventDefault();
|
|
225
|
-
const tableElem =
|
|
226
|
-
const row =
|
|
301
|
+
const tableElem = cell.closest('table');
|
|
302
|
+
const row = cell.closest('tr');
|
|
227
303
|
if (tableElem && row) {
|
|
228
304
|
const rowIndex = parseInt(row.getAttribute('data-row-index') || '0', 10);
|
|
229
305
|
startRowResize(tableElem, rowIndex, e.clientY);
|
|
@@ -239,23 +315,26 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
239
315
|
}
|
|
240
316
|
if (!table)
|
|
241
317
|
return;
|
|
318
|
+
let cursor = '';
|
|
242
319
|
const target = e.target;
|
|
243
|
-
|
|
244
|
-
|
|
320
|
+
const cell = getClosestCell(target);
|
|
321
|
+
if (cell) {
|
|
322
|
+
const rect = cell.getBoundingClientRect();
|
|
245
323
|
const clickX = e.clientX;
|
|
246
324
|
const clickY = e.clientY;
|
|
247
325
|
if (Math.abs(clickX - rect.right) < 5) {
|
|
248
|
-
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
if (Math.abs(clickY - rect.bottom) < 5) {
|
|
252
|
-
el.style.cursor = 'row-resize';
|
|
253
|
-
return;
|
|
326
|
+
cursor = 'col-resize';
|
|
254
327
|
}
|
|
255
|
-
if (
|
|
256
|
-
|
|
328
|
+
else if (Math.abs(clickY - rect.bottom) < 5) {
|
|
329
|
+
cursor = 'row-resize';
|
|
257
330
|
}
|
|
258
331
|
}
|
|
332
|
+
if (cursor) {
|
|
333
|
+
el.style.cursor = cursor;
|
|
334
|
+
}
|
|
335
|
+
else if (el.style.cursor === 'col-resize' || el.style.cursor === 'row-resize') {
|
|
336
|
+
el.style.cursor = '';
|
|
337
|
+
}
|
|
259
338
|
};
|
|
260
339
|
const onMouseUp = () => {
|
|
261
340
|
handleTableResizeEnd();
|
|
@@ -264,15 +343,16 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
264
343
|
if (!table)
|
|
265
344
|
return;
|
|
266
345
|
const target = e.target;
|
|
267
|
-
|
|
268
|
-
|
|
346
|
+
const cell = getClosestCell(target);
|
|
347
|
+
if (cell) {
|
|
348
|
+
const rect = cell.getBoundingClientRect();
|
|
269
349
|
const touch = e.touches[0];
|
|
270
350
|
const clickX = touch.clientX;
|
|
271
351
|
const clickY = touch.clientY;
|
|
272
352
|
if (Math.abs(clickX - rect.right) < 15) {
|
|
273
353
|
e.preventDefault();
|
|
274
|
-
const tableElem =
|
|
275
|
-
const colIndex = parseInt(
|
|
354
|
+
const tableElem = cell.closest('table');
|
|
355
|
+
const colIndex = parseInt(cell.getAttribute('data-col-index') || '0', 10);
|
|
276
356
|
if (tableElem) {
|
|
277
357
|
startColumnResize(tableElem, colIndex, clickX);
|
|
278
358
|
}
|
|
@@ -280,8 +360,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
280
360
|
}
|
|
281
361
|
if (Math.abs(clickY - rect.bottom) < 15) {
|
|
282
362
|
e.preventDefault();
|
|
283
|
-
const tableElem =
|
|
284
|
-
const row =
|
|
363
|
+
const tableElem = cell.closest('table');
|
|
364
|
+
const row = cell.closest('tr');
|
|
285
365
|
if (tableElem && row) {
|
|
286
366
|
const rowIndex = parseInt(row.getAttribute('data-row-index') || '0', 10);
|
|
287
367
|
startRowResize(tableElem, rowIndex, clickY);
|
|
@@ -466,11 +546,432 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
466
546
|
});
|
|
467
547
|
}
|
|
468
548
|
};
|
|
549
|
+
const handlePdfFiles = (files) => {
|
|
550
|
+
if (!files || files.length === 0)
|
|
551
|
+
return;
|
|
552
|
+
const file = files[0];
|
|
553
|
+
if (file.type !== 'application/pdf')
|
|
554
|
+
return;
|
|
555
|
+
// Check if editor has content
|
|
556
|
+
const el = editableRef.current;
|
|
557
|
+
const hasContent = el && el.textContent && el.textContent.trim().length > 0;
|
|
558
|
+
if (hasContent) {
|
|
559
|
+
setPendingImport({ file, type: 'pdf' });
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
processImport(file, 'pdf', 'replace');
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
const processImport = async (file, type, mode) => {
|
|
566
|
+
if (type === 'pdf') {
|
|
567
|
+
await processPdf(file, mode);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
await processDocx(file, mode);
|
|
571
|
+
}
|
|
572
|
+
setPendingImport(null);
|
|
573
|
+
// Reset inputs
|
|
574
|
+
if (pdfInputRef.current)
|
|
575
|
+
pdfInputRef.current.value = "";
|
|
576
|
+
if (docxInputRef.current)
|
|
577
|
+
docxInputRef.current.value = "";
|
|
578
|
+
};
|
|
579
|
+
const processPdf = async (file, mode) => {
|
|
580
|
+
try {
|
|
581
|
+
setLoadingPdf(true);
|
|
582
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
583
|
+
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
|
584
|
+
let fullHtml = '';
|
|
585
|
+
for (let i = 1; i <= pdf.numPages; i++) {
|
|
586
|
+
const page = await pdf.getPage(i);
|
|
587
|
+
const textContent = await page.getTextContent();
|
|
588
|
+
const styles = textContent.styles;
|
|
589
|
+
// 1. Group items into lines
|
|
590
|
+
const items = textContent.items;
|
|
591
|
+
// Calculate base statistics
|
|
592
|
+
const heights = items.map(item => Math.abs(item.transform[3])).filter(h => h > 0);
|
|
593
|
+
heights.sort((a, b) => a - b);
|
|
594
|
+
const medianHeight = heights[Math.floor(heights.length / 2)] || 12;
|
|
595
|
+
// Group by Y (with tolerance)
|
|
596
|
+
const linesMap = new Map();
|
|
597
|
+
for (const item of items) {
|
|
598
|
+
if (!item.str.trim())
|
|
599
|
+
continue;
|
|
600
|
+
// Normalize Y to integer buckets to group roughly
|
|
601
|
+
// PDF Y is bottom-0, so higher Y is higher on page.
|
|
602
|
+
const y = item.transform[5];
|
|
603
|
+
// Find closest existing line
|
|
604
|
+
let foundKey = -1;
|
|
605
|
+
for (const key of linesMap.keys()) {
|
|
606
|
+
if (Math.abs(key - y) < medianHeight * 0.5) {
|
|
607
|
+
foundKey = key;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (foundKey !== -1) {
|
|
612
|
+
linesMap.get(foundKey).items.push(item);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
linesMap.set(y, { y, items: [item] });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Convert map to sorted array (top to bottom)
|
|
619
|
+
const lines = Array.from(linesMap.values()).sort((a, b) => b.y - a.y);
|
|
620
|
+
// Sort items within lines (left to right)
|
|
621
|
+
lines.forEach(line => {
|
|
622
|
+
line.items.sort((a, b) => a.transform[4] - b.transform[4]);
|
|
623
|
+
});
|
|
624
|
+
// 2. Identify and Build Structures
|
|
625
|
+
let html = '';
|
|
626
|
+
let listStack = []; // 'ul' or 'ol'
|
|
627
|
+
let inTable = false;
|
|
628
|
+
let tableColumns = []; // X-coordinates of column starts
|
|
629
|
+
let tableHtml = '';
|
|
630
|
+
const closeList = () => {
|
|
631
|
+
if (listStack.length > 0) {
|
|
632
|
+
html += `</${listStack.pop()}>`;
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
const closeTable = () => {
|
|
636
|
+
if (inTable) {
|
|
637
|
+
html += '<div data-table-wrapper="true" style="overflow-x:auto;width:100%;"><table style="border-collapse:collapse;width:100%;" border="1"><tbody>' + tableHtml + '</tbody></table></div>';
|
|
638
|
+
tableHtml = '';
|
|
639
|
+
inTable = false;
|
|
640
|
+
tableColumns = [];
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
for (let lIndex = 0; lIndex < lines.length; lIndex++) {
|
|
644
|
+
const line = lines[lIndex];
|
|
645
|
+
// Calculate gaps and text
|
|
646
|
+
let lineText = '';
|
|
647
|
+
let lineHtmlContent = '';
|
|
648
|
+
let lastX = -1;
|
|
649
|
+
let gaps = [];
|
|
650
|
+
let itemXs = []; // Start X of logical items (words or phrases)
|
|
651
|
+
// Reconstruct text with spacing detection
|
|
652
|
+
for (let j = 0; j < line.items.length; j++) {
|
|
653
|
+
const item = line.items[j];
|
|
654
|
+
const x = item.transform[4];
|
|
655
|
+
const width = item.width;
|
|
656
|
+
const fontName = item.fontName;
|
|
657
|
+
const fontObj = styles[fontName];
|
|
658
|
+
const isBold = fontObj?.fontFamily?.toLowerCase().includes('bold') || false;
|
|
659
|
+
// const isItalic = fontObj?.fontFamily?.toLowerCase().includes('italic') || false;
|
|
660
|
+
if (lastX > 0) {
|
|
661
|
+
const gap = x - lastX;
|
|
662
|
+
if (gap > 2) { // Minimal space threshold
|
|
663
|
+
lineText += ' ';
|
|
664
|
+
lineHtmlContent += ' ';
|
|
665
|
+
if (gap > 20) { // Large gap threshold for table detection
|
|
666
|
+
gaps.push(gap);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
// First item
|
|
672
|
+
}
|
|
673
|
+
// Track "columns" candidates: items separated by big gaps
|
|
674
|
+
if (j === 0 || (x - lastX) > 20) {
|
|
675
|
+
itemXs.push(x);
|
|
676
|
+
}
|
|
677
|
+
// Append text style
|
|
678
|
+
let chunk = item.str;
|
|
679
|
+
if (isBold)
|
|
680
|
+
chunk = `<strong>${chunk}</strong>`;
|
|
681
|
+
// if (isItalic) chunk = `<em>${chunk}</em>`;
|
|
682
|
+
lineText += item.str;
|
|
683
|
+
lineHtmlContent += chunk;
|
|
684
|
+
lastX = x + width;
|
|
685
|
+
}
|
|
686
|
+
// === Structure Detection ===
|
|
687
|
+
// Max Font Size in line
|
|
688
|
+
const maxH = Math.max(...line.items.map((i) => Math.abs(i.transform[3])));
|
|
689
|
+
const isHeader = maxH > medianHeight * 1.2;
|
|
690
|
+
// List Detection
|
|
691
|
+
const isBullet = /^[•\-\*]\s/.test(lineText);
|
|
692
|
+
const isNumber = /^\d+[\.\)]\s/.test(lineText);
|
|
693
|
+
// Table Detection Logic
|
|
694
|
+
// A line starts a table if it has distinct "columns" (multiple items with large gaps)
|
|
695
|
+
// Or if we are already in a table and this line aligns with columns
|
|
696
|
+
let isTableLine = false;
|
|
697
|
+
// If in table, check alignment
|
|
698
|
+
if (inTable) {
|
|
699
|
+
// Check if items align with known columns
|
|
700
|
+
// Simple loose check: do any of the itemXs align with tableColumns?
|
|
701
|
+
// Or is the line just sparsely populated but roughly compatible?
|
|
702
|
+
// We'll continually simple-add rows for now until a Paragraph break (plain text, no gaps) is found.
|
|
703
|
+
// If line looks like normal paragraph (no large gaps, starts at left margin), close table
|
|
704
|
+
const isPlainParagraph = gaps.length === 0 && itemXs[0] < 50 && lineText.length > 50;
|
|
705
|
+
// Allow wrapping text in table cells, which might look like lines with no gaps?
|
|
706
|
+
// Table wrapping usually is indented or aligns with a column > 0.
|
|
707
|
+
const alignsWithColumn = itemXs.some(x => tableColumns.some(cx => Math.abs(x - cx) < 20));
|
|
708
|
+
if (alignsWithColumn || (itemXs[0] > 50)) {
|
|
709
|
+
isTableLine = true;
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
// Maybe a new row starting at col 0?
|
|
713
|
+
// If it aligns with col 0.
|
|
714
|
+
if (Math.abs(itemXs[0] - tableColumns[0]) < 20) {
|
|
715
|
+
isTableLine = true;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
// Potential start of table: multiple items separated by gaps, AND next line likely follows suit?
|
|
721
|
+
// Or simply: It has > 1 column significantly spaced.
|
|
722
|
+
if (itemXs.length >= 2 && gaps.some(g => g > 30)) {
|
|
723
|
+
isTableLine = true;
|
|
724
|
+
// Establish columns
|
|
725
|
+
tableColumns = [...itemXs];
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
// --- Apply Logic ---
|
|
729
|
+
if (isTableLine) {
|
|
730
|
+
closeList();
|
|
731
|
+
if (!inTable) {
|
|
732
|
+
inTable = true;
|
|
733
|
+
// Start table
|
|
734
|
+
}
|
|
735
|
+
// Build Row
|
|
736
|
+
// We need to map items to cells based on tableColumns.
|
|
737
|
+
// Naive approach: Items close to col X go to col X.
|
|
738
|
+
let rowHtml = '<tr>';
|
|
739
|
+
// We assume `tableColumns` defines the start of each cell.
|
|
740
|
+
// We create a cell for each column.
|
|
741
|
+
// Collect content for each bucket.
|
|
742
|
+
const cellContents = new Array(tableColumns.length).fill('');
|
|
743
|
+
let currentItemHtml = '';
|
|
744
|
+
let currentItemStart = -1;
|
|
745
|
+
// process items again to slot them
|
|
746
|
+
let currentLineX = 0;
|
|
747
|
+
for (const item of line.items) {
|
|
748
|
+
const x = item.transform[4];
|
|
749
|
+
const w = item.width;
|
|
750
|
+
const txt = item.str;
|
|
751
|
+
const fontObj = styles[item.fontName];
|
|
752
|
+
const isBold = fontObj?.fontFamily?.toLowerCase().includes('bold');
|
|
753
|
+
const styledTxt = isBold ? `<strong>${txt}</strong>` : txt;
|
|
754
|
+
// Decide which column this belongs to
|
|
755
|
+
// Find closest column to the left (or close enough)
|
|
756
|
+
let colIdx = 0;
|
|
757
|
+
let minDiff = 9999;
|
|
758
|
+
for (let c = 0; c < tableColumns.length; c++) {
|
|
759
|
+
const colX = tableColumns[c];
|
|
760
|
+
// If item starts near colX or after it (but before next col)
|
|
761
|
+
// Actually, just find the "controlling" column (closest start to the left)
|
|
762
|
+
if (x >= colX - 10) {
|
|
763
|
+
colIdx = c;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// Append space if needed
|
|
767
|
+
if (cellContents[colIdx])
|
|
768
|
+
cellContents[colIdx] += ' ';
|
|
769
|
+
cellContents[colIdx] += styledTxt;
|
|
770
|
+
}
|
|
771
|
+
cellContents.forEach(content => {
|
|
772
|
+
rowHtml += `<td style="border:1px solid #ddd;padding:8px;vertical-align:top;">${content || ' '}</td>`;
|
|
773
|
+
});
|
|
774
|
+
rowHtml += '</tr>';
|
|
775
|
+
tableHtml += rowHtml;
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
closeTable();
|
|
779
|
+
if (isBullet || isNumber) {
|
|
780
|
+
const listType = isBullet ? 'ul' : 'ol';
|
|
781
|
+
if (listStack.length === 0 || listStack[listStack.length - 1] !== listType) {
|
|
782
|
+
if (listStack.length > 0)
|
|
783
|
+
closeList(); // Close switch
|
|
784
|
+
html += `<${listType}>`;
|
|
785
|
+
listStack.push(listType);
|
|
786
|
+
}
|
|
787
|
+
// Strip marker
|
|
788
|
+
const content = lineHtmlContent.replace(/^[•\-\*]|\d+[\.\)]/, '').trim();
|
|
789
|
+
html += `<li>${content}</li>`;
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
closeList();
|
|
793
|
+
if (isHeader) {
|
|
794
|
+
const tag = maxH > medianHeight * 1.5 ? 'h2' : 'h3';
|
|
795
|
+
html += `<${tag}>${lineHtmlContent}</${tag}>`;
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
html += `<p>${lineHtmlContent}</p>`;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
closeList();
|
|
804
|
+
closeTable();
|
|
805
|
+
fullHtml += html;
|
|
806
|
+
}
|
|
807
|
+
const el = editableRef.current;
|
|
808
|
+
if (el) {
|
|
809
|
+
el.focus();
|
|
810
|
+
if (mode === 'replace') {
|
|
811
|
+
// Select all and replace
|
|
812
|
+
const range = document.createRange();
|
|
813
|
+
range.selectNodeContents(el);
|
|
814
|
+
const sel = window.getSelection();
|
|
815
|
+
sel?.removeAllRanges();
|
|
816
|
+
sel?.addRange(range);
|
|
817
|
+
exec("delete"); // Clear content safely
|
|
818
|
+
exec("insertHTML", fullHtml);
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
// Append
|
|
822
|
+
const range = document.createRange();
|
|
823
|
+
range.selectNodeContents(el);
|
|
824
|
+
range.collapse(false); // End
|
|
825
|
+
const sel = window.getSelection();
|
|
826
|
+
sel?.removeAllRanges();
|
|
827
|
+
sel?.addRange(range);
|
|
828
|
+
exec("insertHTML", "<br>" + fullHtml);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
catch (error) {
|
|
833
|
+
console.error('Error reading PDF:', error);
|
|
834
|
+
// Optional: show user error
|
|
835
|
+
}
|
|
836
|
+
finally {
|
|
837
|
+
setLoadingPdf(false);
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
const handleDocxFiles = (files) => {
|
|
841
|
+
if (!files || files.length === 0)
|
|
842
|
+
return;
|
|
843
|
+
const file = files[0];
|
|
844
|
+
if (!file.name.endsWith('.docx'))
|
|
845
|
+
return;
|
|
846
|
+
// Check if editor has content
|
|
847
|
+
const el = editableRef.current;
|
|
848
|
+
const hasContent = el && el.textContent && el.textContent.trim().length > 0;
|
|
849
|
+
if (hasContent) {
|
|
850
|
+
setPendingImport({ file, type: 'docx' });
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
processImport(file, 'docx', 'replace');
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
const processDocx = async (file, mode) => {
|
|
857
|
+
try {
|
|
858
|
+
setLoadingDocx(true);
|
|
859
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
860
|
+
const result = await mammoth.convertToHtml({ arrayBuffer });
|
|
861
|
+
let html = result.value;
|
|
862
|
+
if (html) {
|
|
863
|
+
// Process HTML to ensure tables have borders and structure
|
|
864
|
+
const temp = document.createElement('div');
|
|
865
|
+
temp.innerHTML = html;
|
|
866
|
+
const tables = temp.querySelectorAll('table');
|
|
867
|
+
tables.forEach(tbl => {
|
|
868
|
+
tbl.style.borderCollapse = 'collapse';
|
|
869
|
+
tbl.style.minWidth = '100%';
|
|
870
|
+
// Browser parser auto-adds tbody, but we verify styles
|
|
871
|
+
const cells = tbl.querySelectorAll('td, th');
|
|
872
|
+
cells.forEach(cell => {
|
|
873
|
+
cell.style.border = '1px solid #000';
|
|
874
|
+
cell.style.padding = '8px';
|
|
875
|
+
cell.style.verticalAlign = 'top';
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
html = temp.innerHTML;
|
|
879
|
+
const el = editableRef.current;
|
|
880
|
+
if (el) {
|
|
881
|
+
el.focus();
|
|
882
|
+
if (mode === 'replace') {
|
|
883
|
+
const range = document.createRange();
|
|
884
|
+
range.selectNodeContents(el);
|
|
885
|
+
const sel = window.getSelection();
|
|
886
|
+
sel?.removeAllRanges();
|
|
887
|
+
sel?.addRange(range);
|
|
888
|
+
exec("delete");
|
|
889
|
+
exec("insertHTML", html);
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
const range = document.createRange();
|
|
893
|
+
range.selectNodeContents(el);
|
|
894
|
+
range.collapse(false);
|
|
895
|
+
const sel = window.getSelection();
|
|
896
|
+
sel?.removeAllRanges();
|
|
897
|
+
sel?.addRange(range);
|
|
898
|
+
exec("insertHTML", "<br>" + html);
|
|
899
|
+
}
|
|
900
|
+
// Initialize handlers for the new content
|
|
901
|
+
// We use setTimeout to let the DOM settle after execCommand
|
|
902
|
+
setTimeout(() => {
|
|
903
|
+
ensureTableWrappers(el);
|
|
904
|
+
addTableResizeHandles();
|
|
905
|
+
fixNegativeMargins(el);
|
|
906
|
+
handleInput();
|
|
907
|
+
}, 10);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
catch (error) {
|
|
912
|
+
console.error('Error reading DOCX:', error);
|
|
913
|
+
}
|
|
914
|
+
finally {
|
|
915
|
+
setLoadingDocx(false);
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
const fixNegativeMargins = (root) => {
|
|
919
|
+
try {
|
|
920
|
+
const nodes = root.querySelectorAll('*');
|
|
921
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
922
|
+
const node = nodes[i];
|
|
923
|
+
if (node.style && node.style.marginLeft && node.style.marginLeft.trim().startsWith('-')) {
|
|
924
|
+
node.style.marginLeft = '0px';
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
catch { }
|
|
929
|
+
};
|
|
930
|
+
const ensureTableWrappers = (root) => {
|
|
931
|
+
try {
|
|
932
|
+
const tables = root.querySelectorAll('table');
|
|
933
|
+
tables.forEach((table) => {
|
|
934
|
+
const parent = table.parentElement;
|
|
935
|
+
if (parent && parent.getAttribute('data-table-wrapper') !== 'true') {
|
|
936
|
+
const wrapper = document.createElement('div');
|
|
937
|
+
wrapper.setAttribute('data-table-wrapper', 'true');
|
|
938
|
+
wrapper.style.overflowX = 'auto';
|
|
939
|
+
wrapper.style.webkitOverflowScrolling = 'touch';
|
|
940
|
+
wrapper.style.width = '100%';
|
|
941
|
+
wrapper.style.maxWidth = '100%';
|
|
942
|
+
wrapper.style.display = 'block';
|
|
943
|
+
// Use insertBefore + appendChild to move element without losing too much state
|
|
944
|
+
// simpler than replaceChild for wrapping
|
|
945
|
+
parent.insertBefore(wrapper, table);
|
|
946
|
+
wrapper.appendChild(table);
|
|
947
|
+
}
|
|
948
|
+
// Always ensure table takes full width
|
|
949
|
+
if (table.style.width !== '100%') {
|
|
950
|
+
table.style.width = '100%';
|
|
951
|
+
}
|
|
952
|
+
// Ensure min-width is set
|
|
953
|
+
if (!table.style.minWidth || table.style.minWidth === '0px') {
|
|
954
|
+
table.style.minWidth = '100%';
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
catch (e) {
|
|
959
|
+
console.error("Error wrapping tables", e);
|
|
960
|
+
}
|
|
961
|
+
};
|
|
469
962
|
const handleInput = () => {
|
|
470
963
|
if (isComposingRef.current)
|
|
471
964
|
return;
|
|
472
965
|
const el = editableRef.current;
|
|
473
|
-
if (!el
|
|
966
|
+
if (!el)
|
|
967
|
+
return;
|
|
968
|
+
// Auto-fix negative margins that might cause visibility issues
|
|
969
|
+
fixNegativeMargins(el);
|
|
970
|
+
// Ensure tables are wrapped for horizontal scrolling
|
|
971
|
+
ensureTableWrappers(el);
|
|
972
|
+
// Add resize handles to tables
|
|
973
|
+
addTableResizeHandles();
|
|
974
|
+
if (!onChange)
|
|
474
975
|
return;
|
|
475
976
|
const html = el.innerHTML;
|
|
476
977
|
if (html !== lastEmittedRef.current) {
|
|
@@ -481,7 +982,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
481
982
|
const buildTableHTML = (rows, cols) => {
|
|
482
983
|
const safeRows = Math.max(1, Math.min(50, Math.floor(rows) || 1));
|
|
483
984
|
const safeCols = Math.max(1, Math.min(20, Math.floor(cols) || 1));
|
|
484
|
-
let html = '<table style="border-collapse:collapse;width:100%;"><tbody>';
|
|
985
|
+
let html = '<div data-table-wrapper="true" style="overflow-x:auto;-webkit-overflow-scrolling:touch;width:100%;max-width:100%;display:block;"><table style="border-collapse:collapse;min-width:100%;"><tbody>';
|
|
485
986
|
for (let r = 0; r < safeRows; r++) {
|
|
486
987
|
html += "<tr>";
|
|
487
988
|
for (let c = 0; c < safeCols; c++) {
|
|
@@ -490,7 +991,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
490
991
|
}
|
|
491
992
|
html += "</tr>";
|
|
492
993
|
}
|
|
493
|
-
html += "</tbody></table>";
|
|
994
|
+
html += "</tbody></table></div>";
|
|
494
995
|
return html;
|
|
495
996
|
};
|
|
496
997
|
const insertTable = () => {
|
|
@@ -895,6 +1396,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
895
1396
|
return;
|
|
896
1397
|
const firstCell = cells[0];
|
|
897
1398
|
const currentWidth = firstCell.offsetWidth;
|
|
1399
|
+
// Unlock table width so it can grow
|
|
1400
|
+
table.style.width = "max-content";
|
|
1401
|
+
table.style.minWidth = "100%";
|
|
898
1402
|
tableResizeRef.current = {
|
|
899
1403
|
type: 'column',
|
|
900
1404
|
table,
|
|
@@ -991,14 +1495,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
991
1495
|
});
|
|
992
1496
|
});
|
|
993
1497
|
};
|
|
994
|
-
return (_jsxs("div", { style: {
|
|
1498
|
+
return (_jsxs("div", { style: {
|
|
1499
|
+
border: "1px solid #ddd",
|
|
1500
|
+
borderRadius: 6,
|
|
1501
|
+
width: "100%",
|
|
1502
|
+
maxWidth: "100vw",
|
|
1503
|
+
overflow: "hidden",
|
|
1504
|
+
display: "flex",
|
|
1505
|
+
flexDirection: "column",
|
|
1506
|
+
background: "#fff",
|
|
1507
|
+
boxSizing: "border-box"
|
|
1508
|
+
}, children: [_jsxs("div", { style: {
|
|
995
1509
|
display: "flex",
|
|
996
1510
|
flexWrap: "wrap",
|
|
1511
|
+
maxWidth: "100%",
|
|
997
1512
|
gap: 8,
|
|
998
1513
|
padding: 8,
|
|
999
1514
|
borderBottom: "1px solid #eee",
|
|
1000
|
-
background: "#fff",
|
|
1001
|
-
color: "#111",
|
|
1002
1515
|
position: "sticky",
|
|
1003
1516
|
top: 0,
|
|
1004
1517
|
zIndex: 1,
|
|
@@ -1028,7 +1541,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1028
1541
|
}
|
|
1029
1542
|
}
|
|
1030
1543
|
e.currentTarget.value = "";
|
|
1031
|
-
} })),
|
|
1544
|
+
} })), _jsx("input", { ref: pdfInputRef, type: "file", accept: "application/pdf", style: { display: "none" }, onChange: (e) => {
|
|
1545
|
+
handlePdfFiles(e.currentTarget.files);
|
|
1546
|
+
e.currentTarget.value = "";
|
|
1547
|
+
} }), _jsx("input", { ref: docxInputRef, type: "file", accept: ".docx", style: { display: "none" }, onChange: (e) => {
|
|
1548
|
+
handleDocxFiles(e.currentTarget.files);
|
|
1549
|
+
e.currentTarget.value = "";
|
|
1550
|
+
} }), _jsxs("select", { defaultValue: "p", onChange: (e) => {
|
|
1032
1551
|
const val = e.target.value;
|
|
1033
1552
|
if (val === "p")
|
|
1034
1553
|
applyFormatBlock("<p>");
|
|
@@ -1097,7 +1616,24 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1097
1616
|
borderRadius: 6,
|
|
1098
1617
|
background: "#fff",
|
|
1099
1618
|
color: "#111",
|
|
1100
|
-
}, children: [_jsx("option", { value: "8", children: "8" }), _jsx("option", { value: "9", children: "9" }), _jsx("option", { value: "10", children: "10" }), _jsx("option", { value: "11", children: "11" }), _jsx("option", { value: "12", children: "12" }), _jsx("option", { value: "14", children: "14" }), _jsx("option", { value: "18", children: "18" }), _jsx("option", { value: "24", children: "24" }), _jsx("option", { value: "30", children: "30" }), _jsx("option", { value: "36", children: "36" }), _jsx("option", { value: "48", children: "48" }), _jsx("option", { value: "60", children: "60" }), _jsx("option", { value: "72", children: "72" }), _jsx("option", { value: "96", children: "96" })] }), _jsxs("
|
|
1619
|
+
}, children: [_jsx("option", { value: "", disabled: true, children: "Size" }), _jsx("option", { value: "8", children: "8" }), _jsx("option", { value: "9", children: "9" }), _jsx("option", { value: "10", children: "10" }), _jsx("option", { value: "11", children: "11" }), _jsx("option", { value: "12", children: "12" }), _jsx("option", { value: "14", children: "14" }), _jsx("option", { value: "18", children: "18" }), _jsx("option", { value: "24", children: "24" }), _jsx("option", { value: "30", children: "30" }), _jsx("option", { value: "36", children: "36" }), _jsx("option", { value: "48", children: "48" }), _jsx("option", { value: "60", children: "60" }), _jsx("option", { value: "72", children: "72" }), _jsx("option", { value: "96", children: "96" })] }), _jsxs("select", { value: currentFont, onMouseDown: () => {
|
|
1620
|
+
const sel = window.getSelection();
|
|
1621
|
+
if (sel && sel.rangeCount > 0) {
|
|
1622
|
+
const range = sel.getRangeAt(0);
|
|
1623
|
+
const editor = editableRef.current;
|
|
1624
|
+
if (editor && editor.contains(range.commonAncestorContainer) && !range.collapsed) {
|
|
1625
|
+
savedRangeRef.current = range.cloneRange();
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}, onChange: (e) => applyFontFamily(e.target.value), title: "Font Family", style: {
|
|
1629
|
+
height: 32,
|
|
1630
|
+
padding: "0 8px",
|
|
1631
|
+
border: "1px solid #e5e7eb",
|
|
1632
|
+
borderRadius: 6,
|
|
1633
|
+
background: "#fff",
|
|
1634
|
+
color: "#111",
|
|
1635
|
+
maxWidth: 100,
|
|
1636
|
+
}, children: [_jsx("option", { value: "", disabled: true, children: "Font" }), fonts.map((f) => (_jsx("option", { value: f.value, children: f.name }, f.value)))] }), _jsx("button", { title: "Text Color", onClick: () => {
|
|
1101
1637
|
setColorPickerType('text');
|
|
1102
1638
|
setShowColorPicker(true);
|
|
1103
1639
|
}, style: {
|
|
@@ -1109,16 +1645,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1109
1645
|
background: "#fff",
|
|
1110
1646
|
color: "#111",
|
|
1111
1647
|
position: "relative",
|
|
1112
|
-
}, children:
|
|
1113
|
-
position: "absolute",
|
|
1114
|
-
bottom: 4,
|
|
1115
|
-
left: "50%",
|
|
1116
|
-
transform: "translateX(-50%)",
|
|
1117
|
-
width: 16,
|
|
1118
|
-
height: 3,
|
|
1119
|
-
background: "#000",
|
|
1120
|
-
borderRadius: 1,
|
|
1121
|
-
} })] }), _jsx("button", { title: "Background Color", onClick: () => {
|
|
1648
|
+
}, children: _jsx("span", { style: { fontWeight: 700 }, children: "A" }) }), _jsx("button", { title: "Background Color", onClick: () => {
|
|
1122
1649
|
setColorPickerType('background');
|
|
1123
1650
|
setShowColorPicker(true);
|
|
1124
1651
|
}, style: {
|
|
@@ -1129,7 +1656,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1129
1656
|
borderRadius: 6,
|
|
1130
1657
|
background: "#fff",
|
|
1131
1658
|
color: "#111",
|
|
1132
|
-
}, children: _jsx("span", { style: { fontWeight: 700,
|
|
1659
|
+
}, children: _jsx("span", { style: { fontWeight: 700, padding: "2px 4px" }, children: "A" }) }), _jsx("button", { title: "Bulleted list", onClick: () => exec("insertUnorderedList"), style: {
|
|
1133
1660
|
height: 32,
|
|
1134
1661
|
padding: "0 10px",
|
|
1135
1662
|
border: "1px solid #e5e7eb",
|
|
@@ -1196,7 +1723,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1196
1723
|
borderRadius: 6,
|
|
1197
1724
|
background: "#fff",
|
|
1198
1725
|
color: "#111",
|
|
1199
|
-
}, children: "\uD83D\uDCC1 Media" })),
|
|
1726
|
+
}, children: "\uD83D\uDCC1 Media" })), _jsx("button", { title: "Import PDF", onClick: () => pdfInputRef.current?.click(), disabled: loadingPdf, style: {
|
|
1727
|
+
height: 32,
|
|
1728
|
+
padding: "0 10px",
|
|
1729
|
+
border: "1px solid #e5e7eb",
|
|
1730
|
+
borderRadius: 6,
|
|
1731
|
+
background: "#fff",
|
|
1732
|
+
color: "#111",
|
|
1733
|
+
opacity: loadingPdf ? 0.5 : 1,
|
|
1734
|
+
}, children: loadingPdf ? '⌛ Importing...' : '📄 PDF' }), _jsx("button", { title: "Import DOCX", onClick: () => docxInputRef.current?.click(), disabled: loadingDocx, style: {
|
|
1735
|
+
height: 32,
|
|
1736
|
+
padding: "0 10px",
|
|
1737
|
+
border: "1px solid #e5e7eb",
|
|
1738
|
+
borderRadius: 6,
|
|
1739
|
+
background: "#fff",
|
|
1740
|
+
color: "#111",
|
|
1741
|
+
opacity: loadingDocx ? 0.5 : 1,
|
|
1742
|
+
}, children: loadingDocx ? '⌛ Importing...' : '📝 DOCX' }), _jsxs("div", { style: {
|
|
1200
1743
|
display: "inline-flex",
|
|
1201
1744
|
gap: 4,
|
|
1202
1745
|
alignItems: "center",
|
|
@@ -1319,7 +1862,60 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1319
1862
|
}, children: [_jsx("button", { onClick: () => setShowTableDialog(false), children: "Cancel" }), _jsx("button", { onClick: () => {
|
|
1320
1863
|
insertTable();
|
|
1321
1864
|
setShowTableDialog(false);
|
|
1322
|
-
}, children: "Insert" })] })] }) })),
|
|
1865
|
+
}, children: "Insert" })] })] }) })), pendingImport && (_jsx("div", { style: {
|
|
1866
|
+
position: "fixed",
|
|
1867
|
+
inset: 0,
|
|
1868
|
+
background: "rgba(0,0,0,0.5)",
|
|
1869
|
+
display: "flex",
|
|
1870
|
+
alignItems: "center",
|
|
1871
|
+
justifyContent: "center",
|
|
1872
|
+
zIndex: 100,
|
|
1873
|
+
}, onClick: () => {
|
|
1874
|
+
setPendingImport(null);
|
|
1875
|
+
if (pdfInputRef.current)
|
|
1876
|
+
pdfInputRef.current.value = "";
|
|
1877
|
+
if (docxInputRef.current)
|
|
1878
|
+
docxInputRef.current.value = "";
|
|
1879
|
+
}, children: _jsxs("div", { style: {
|
|
1880
|
+
background: "#fff",
|
|
1881
|
+
padding: 20,
|
|
1882
|
+
borderRadius: 8,
|
|
1883
|
+
maxWidth: 400,
|
|
1884
|
+
width: "90%",
|
|
1885
|
+
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
|
|
1886
|
+
}, onClick: (e) => e.stopPropagation(), children: [_jsx("h3", { style: { margin: "0 0 12px 0", fontSize: 18, fontWeight: 600 }, children: "Import Content" }), _jsx("p", { style: { margin: "0 0 20px 0", color: "#4b5563", fontSize: 14 }, children: "The editor already contains content. How would you like to handle the imported document?" }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [_jsx("button", { onClick: () => processImport(pendingImport.file, pendingImport.type, 'replace'), style: {
|
|
1887
|
+
padding: "8px 16px",
|
|
1888
|
+
background: "#dc2626",
|
|
1889
|
+
color: "white",
|
|
1890
|
+
border: "none",
|
|
1891
|
+
borderRadius: 6,
|
|
1892
|
+
cursor: "pointer",
|
|
1893
|
+
fontWeight: 500,
|
|
1894
|
+
textAlign: "left"
|
|
1895
|
+
}, children: "Replace Check existing content (Overwrite)" }), _jsx("button", { onClick: () => processImport(pendingImport.file, pendingImport.type, 'append'), style: {
|
|
1896
|
+
padding: "8px 16px",
|
|
1897
|
+
background: "#2563eb",
|
|
1898
|
+
color: "white",
|
|
1899
|
+
border: "none",
|
|
1900
|
+
borderRadius: 6,
|
|
1901
|
+
cursor: "pointer",
|
|
1902
|
+
fontWeight: 500,
|
|
1903
|
+
textAlign: "left"
|
|
1904
|
+
}, children: "Append to bottom" }), _jsx("button", { onClick: () => {
|
|
1905
|
+
setPendingImport(null);
|
|
1906
|
+
if (pdfInputRef.current)
|
|
1907
|
+
pdfInputRef.current.value = "";
|
|
1908
|
+
if (docxInputRef.current)
|
|
1909
|
+
docxInputRef.current.value = "";
|
|
1910
|
+
}, style: {
|
|
1911
|
+
padding: "8px 16px",
|
|
1912
|
+
background: "#f3f4f6",
|
|
1913
|
+
color: "#374151",
|
|
1914
|
+
border: "1px solid #d1d5db",
|
|
1915
|
+
borderRadius: 6,
|
|
1916
|
+
cursor: "pointer",
|
|
1917
|
+
marginTop: 4
|
|
1918
|
+
}, children: "Cancel" })] })] }) })), showColorPicker && (_jsx("div", { style: {
|
|
1323
1919
|
position: "fixed",
|
|
1324
1920
|
inset: 0,
|
|
1325
1921
|
background: "rgba(0,0,0,0.35)",
|
|
@@ -1332,6 +1928,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1332
1928
|
padding: 16,
|
|
1333
1929
|
borderRadius: 8,
|
|
1334
1930
|
minWidth: 320,
|
|
1931
|
+
maxWidth: "90vw",
|
|
1335
1932
|
color: "#000",
|
|
1336
1933
|
}, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 12 }, children: colorPickerType === 'text' ? 'Select Text Color' : 'Select Background Color' }), _jsx("div", { style: {
|
|
1337
1934
|
display: "grid",
|
|
@@ -1485,273 +2082,285 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1485
2082
|
borderRadius: 4,
|
|
1486
2083
|
background: "#fff",
|
|
1487
2084
|
color: "#000",
|
|
1488
|
-
}, title: sym, children: sym }, i)))] })] }) })), _jsx("div", {
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
2085
|
+
}, title: sym, children: sym }, i)))] })] }) })), _jsx("div", { style: {
|
|
2086
|
+
width: "100%",
|
|
2087
|
+
maxWidth: "100%",
|
|
2088
|
+
flex: "1 1 auto",
|
|
2089
|
+
minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
|
|
2090
|
+
maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
|
|
2091
|
+
overflowY: "auto",
|
|
2092
|
+
overflowX: "hidden",
|
|
2093
|
+
boxSizing: "border-box",
|
|
2094
|
+
position: "relative",
|
|
2095
|
+
}, children: _jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onInput: handleInput, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
|
|
2096
|
+
isComposingRef.current = false;
|
|
2097
|
+
handleInput();
|
|
2098
|
+
}, onPaste: (e) => {
|
|
2099
|
+
const items = e.clipboardData?.files;
|
|
2100
|
+
if (media && items && items.length) {
|
|
2101
|
+
const hasImage = Array.from(items).some((f) => f.type.startsWith("image/"));
|
|
2102
|
+
if (hasImage) {
|
|
2103
|
+
e.preventDefault();
|
|
2104
|
+
handleLocalImageFiles(items);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
}, onDragOver: (e) => {
|
|
2108
|
+
// Allow dragging images within editor and file drops
|
|
2109
|
+
if (draggedImageRef.current ||
|
|
2110
|
+
e.dataTransfer?.types?.includes("Files")) {
|
|
1496
2111
|
e.preventDefault();
|
|
1497
|
-
handleLocalImageFiles(items);
|
|
1498
2112
|
}
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
}, onDrop: (e) => {
|
|
1507
|
-
// Move existing dragged image inside editor
|
|
1508
|
-
if (draggedImageRef.current) {
|
|
1509
|
-
e.preventDefault();
|
|
1510
|
-
const x = e.clientX;
|
|
1511
|
-
const y = e.clientY;
|
|
1512
|
-
let range = null;
|
|
1513
|
-
// @ts-ignore
|
|
1514
|
-
if (document.caretRangeFromPoint) {
|
|
2113
|
+
}, onDrop: (e) => {
|
|
2114
|
+
// Move existing dragged image inside editor
|
|
2115
|
+
if (draggedImageRef.current) {
|
|
2116
|
+
e.preventDefault();
|
|
2117
|
+
const x = e.clientX;
|
|
2118
|
+
const y = e.clientY;
|
|
2119
|
+
let range = null;
|
|
1515
2120
|
// @ts-ignore
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
const pos = document.caretPositionFromPoint(x, y);
|
|
1520
|
-
if (pos) {
|
|
1521
|
-
range = document.createRange();
|
|
1522
|
-
range.setStart(pos.offsetNode, pos.offset);
|
|
2121
|
+
if (document.caretRangeFromPoint) {
|
|
2122
|
+
// @ts-ignore
|
|
2123
|
+
range = document.caretRangeFromPoint(x, y);
|
|
1523
2124
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
editableRef.current?.contains(range.commonAncestorContainer)) {
|
|
1530
|
-
// Avoid inserting inside the image itself
|
|
1531
|
-
if (range.startContainer === img || range.endContainer === img)
|
|
1532
|
-
return;
|
|
1533
|
-
// If dropping inside a link, insert right after the link element
|
|
1534
|
-
let container = range.commonAncestorContainer;
|
|
1535
|
-
let linkAncestor = null;
|
|
1536
|
-
let el = container;
|
|
1537
|
-
while (el && el !== editableRef.current) {
|
|
1538
|
-
if (el.tagName === "A") {
|
|
1539
|
-
linkAncestor = el;
|
|
1540
|
-
break;
|
|
2125
|
+
else if (document.caretPositionFromPoint) {
|
|
2126
|
+
const pos = document.caretPositionFromPoint(x, y);
|
|
2127
|
+
if (pos) {
|
|
2128
|
+
range = document.createRange();
|
|
2129
|
+
range.setStart(pos.offsetNode, pos.offset);
|
|
1541
2130
|
}
|
|
1542
|
-
el = el.parentElement;
|
|
1543
|
-
}
|
|
1544
|
-
if (linkAncestor) {
|
|
1545
|
-
linkAncestor.parentElement?.insertBefore(img, linkAncestor.nextSibling);
|
|
1546
2131
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
2132
|
+
const img = draggedImageRef.current;
|
|
2133
|
+
draggedImageRef.current = null;
|
|
2134
|
+
if (range &&
|
|
2135
|
+
img &&
|
|
2136
|
+
editableRef.current?.contains(range.commonAncestorContainer)) {
|
|
2137
|
+
// Avoid inserting inside the image itself
|
|
2138
|
+
if (range.startContainer === img || range.endContainer === img)
|
|
2139
|
+
return;
|
|
2140
|
+
// If dropping inside a link, insert right after the link element
|
|
2141
|
+
let container = range.commonAncestorContainer;
|
|
2142
|
+
let linkAncestor = null;
|
|
2143
|
+
let el = container;
|
|
2144
|
+
while (el && el !== editableRef.current) {
|
|
2145
|
+
if (el.tagName === "A") {
|
|
2146
|
+
linkAncestor = el;
|
|
2147
|
+
break;
|
|
2148
|
+
}
|
|
2149
|
+
el = el.parentElement;
|
|
2150
|
+
}
|
|
2151
|
+
if (linkAncestor) {
|
|
2152
|
+
linkAncestor.parentElement?.insertBefore(img, linkAncestor.nextSibling);
|
|
2153
|
+
}
|
|
2154
|
+
else {
|
|
2155
|
+
range.insertNode(img);
|
|
2156
|
+
}
|
|
2157
|
+
const r = document.createRange();
|
|
2158
|
+
r.setStartAfter(img);
|
|
2159
|
+
r.collapse(true);
|
|
2160
|
+
safeSelectRange(r);
|
|
2161
|
+
setSelectedImage(img);
|
|
2162
|
+
scheduleImageOverlay();
|
|
2163
|
+
handleInput();
|
|
1549
2164
|
}
|
|
1550
|
-
|
|
1551
|
-
r.setStartAfter(img);
|
|
1552
|
-
r.collapse(true);
|
|
1553
|
-
safeSelectRange(r);
|
|
1554
|
-
setSelectedImage(img);
|
|
1555
|
-
scheduleImageOverlay();
|
|
1556
|
-
handleInput();
|
|
2165
|
+
return;
|
|
1557
2166
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
const y = e.clientY;
|
|
1565
|
-
let range = null;
|
|
1566
|
-
// @ts-ignore
|
|
1567
|
-
if (document.caretRangeFromPoint) {
|
|
2167
|
+
if (media && e.dataTransfer?.files?.length) {
|
|
2168
|
+
e.preventDefault();
|
|
2169
|
+
// Try to move caret to drop point
|
|
2170
|
+
const x = e.clientX;
|
|
2171
|
+
const y = e.clientY;
|
|
2172
|
+
let range = null;
|
|
1568
2173
|
// @ts-ignore
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
const pos = document.caretPositionFromPoint(x, y);
|
|
1573
|
-
if (pos) {
|
|
1574
|
-
range = document.createRange();
|
|
1575
|
-
range.setStart(pos.offsetNode, pos.offset);
|
|
2174
|
+
if (document.caretRangeFromPoint) {
|
|
2175
|
+
// @ts-ignore
|
|
2176
|
+
range = document.caretRangeFromPoint(x, y);
|
|
1576
2177
|
}
|
|
2178
|
+
else if (document.caretPositionFromPoint) {
|
|
2179
|
+
const pos = document.caretPositionFromPoint(x, y);
|
|
2180
|
+
if (pos) {
|
|
2181
|
+
range = document.createRange();
|
|
2182
|
+
range.setStart(pos.offsetNode, pos.offset);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
if (range) {
|
|
2186
|
+
const sel = window.getSelection();
|
|
2187
|
+
sel?.removeAllRanges();
|
|
2188
|
+
sel?.addRange(range);
|
|
2189
|
+
}
|
|
2190
|
+
handleLocalImageFiles(e.dataTransfer.files);
|
|
1577
2191
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
2192
|
+
}, onClick: (e) => {
|
|
2193
|
+
const t = e.target;
|
|
2194
|
+
if (t && t.tagName === "IMG") {
|
|
2195
|
+
setSelectedImage(t);
|
|
2196
|
+
scheduleImageOverlay();
|
|
1582
2197
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
const dt = e.dataTransfer;
|
|
1604
|
-
if (dt && typeof dt.setDragImage === "function") {
|
|
1605
|
-
const ghost = new Image();
|
|
1606
|
-
ghost.src = t.src;
|
|
1607
|
-
ghost.width = Math.min(120, t.width);
|
|
1608
|
-
ghost.height = Math.min(120, t.height);
|
|
1609
|
-
dt.setDragImage(ghost, 10, 10);
|
|
2198
|
+
else {
|
|
2199
|
+
setSelectedImage(null);
|
|
2200
|
+
setImageOverlay(null);
|
|
2201
|
+
}
|
|
2202
|
+
}, onDragStart: (e) => {
|
|
2203
|
+
const t = e.target;
|
|
2204
|
+
if (t && t.tagName === "IMG") {
|
|
2205
|
+
draggedImageRef.current = t;
|
|
2206
|
+
try {
|
|
2207
|
+
e.dataTransfer?.setData("text/plain", "moving-image");
|
|
2208
|
+
e.dataTransfer.effectAllowed = "move";
|
|
2209
|
+
// Provide a subtle drag image
|
|
2210
|
+
const dt = e.dataTransfer;
|
|
2211
|
+
if (dt && typeof dt.setDragImage === "function") {
|
|
2212
|
+
const ghost = new Image();
|
|
2213
|
+
ghost.src = t.src;
|
|
2214
|
+
ghost.width = Math.min(120, t.width);
|
|
2215
|
+
ghost.height = Math.min(120, t.height);
|
|
2216
|
+
dt.setDragImage(ghost, 10, 10);
|
|
2217
|
+
}
|
|
1610
2218
|
}
|
|
2219
|
+
catch { }
|
|
1611
2220
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
2221
|
+
else {
|
|
2222
|
+
draggedImageRef.current = null;
|
|
2223
|
+
}
|
|
2224
|
+
}, onDragEnd: () => {
|
|
1615
2225
|
draggedImageRef.current = null;
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
el.innerHTML = "<p><br></p>";
|
|
1631
|
-
}
|
|
1632
|
-
}, onKeyDown: (e) => {
|
|
1633
|
-
if (formula &&
|
|
1634
|
-
(e.metaKey || e.ctrlKey) &&
|
|
1635
|
-
String(e.key).toLowerCase() === "m") {
|
|
1636
|
-
e.preventDefault();
|
|
1637
|
-
setShowFormulaDialog(true);
|
|
1638
|
-
return;
|
|
1639
|
-
}
|
|
1640
|
-
// Keep Tab for indentation in lists; otherwise insert 2 spaces
|
|
1641
|
-
if (e.key === "Tab") {
|
|
1642
|
-
e.preventDefault();
|
|
1643
|
-
if (document.queryCommandState("insertUnorderedList") ||
|
|
1644
|
-
document.queryCommandState("insertOrderedList")) {
|
|
1645
|
-
exec(e.shiftKey ? "outdent" : "indent");
|
|
2226
|
+
}, style: {
|
|
2227
|
+
minHeight: "100%",
|
|
2228
|
+
maxWidth: "100%",
|
|
2229
|
+
overflowX: "hidden",
|
|
2230
|
+
padding: "16px",
|
|
2231
|
+
outline: "none",
|
|
2232
|
+
lineHeight: 1.6,
|
|
2233
|
+
boxSizing: "border-box",
|
|
2234
|
+
fontFamily: defaultFont || "inherit",
|
|
2235
|
+
}, "data-placeholder": placeholder, onFocus: (e) => {
|
|
2236
|
+
// Ensure the editor has at least one paragraph to type into
|
|
2237
|
+
const el = e.currentTarget;
|
|
2238
|
+
if (!el.innerHTML || el.innerHTML === "<br>") {
|
|
2239
|
+
el.innerHTML = "<p><br></p>";
|
|
1646
2240
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
2241
|
+
}, onKeyDown: (e) => {
|
|
2242
|
+
if (formula &&
|
|
2243
|
+
(e.metaKey || e.ctrlKey) &&
|
|
2244
|
+
String(e.key).toLowerCase() === "m") {
|
|
2245
|
+
e.preventDefault();
|
|
2246
|
+
setShowFormulaDialog(true);
|
|
2247
|
+
return;
|
|
1649
2248
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
cell &&
|
|
1657
|
-
cell.parentElement &&
|
|
1658
|
-
cell.parentElement.parentElement) {
|
|
1659
|
-
const row = cell.parentElement;
|
|
1660
|
-
const tbody = row.parentElement;
|
|
1661
|
-
const cells = Array.from(row.children).filter((c) => c.tagName === "TD" ||
|
|
1662
|
-
c.tagName === "TH");
|
|
1663
|
-
const rows = Array.from(tbody.children);
|
|
1664
|
-
const rIdx = rows.indexOf(row);
|
|
1665
|
-
const cIdx = cells.indexOf(cell);
|
|
1666
|
-
const atStart = (sel?.anchorOffset || 0) === 0;
|
|
1667
|
-
const cellTextLen = (cell.textContent || "").length;
|
|
1668
|
-
const atEnd = (sel?.anchorOffset || 0) >= cellTextLen;
|
|
1669
|
-
let target = null;
|
|
1670
|
-
if (e.key === "ArrowLeft" && atStart && cIdx > 0) {
|
|
1671
|
-
target = row.children[cIdx - 1];
|
|
1672
|
-
}
|
|
1673
|
-
else if (e.key === "ArrowRight" &&
|
|
1674
|
-
atEnd &&
|
|
1675
|
-
cIdx < row.children.length - 1) {
|
|
1676
|
-
target = row.children[cIdx + 1];
|
|
1677
|
-
}
|
|
1678
|
-
else if (e.key === "ArrowUp" && rIdx > 0 && atStart) {
|
|
1679
|
-
target = rows[rIdx - 1].children[cIdx];
|
|
2249
|
+
// Keep Tab for indentation in lists; otherwise insert 2 spaces
|
|
2250
|
+
if (e.key === "Tab") {
|
|
2251
|
+
e.preventDefault();
|
|
2252
|
+
if (document.queryCommandState("insertUnorderedList") ||
|
|
2253
|
+
document.queryCommandState("insertOrderedList")) {
|
|
2254
|
+
exec(e.shiftKey ? "outdent" : "indent");
|
|
1680
2255
|
}
|
|
1681
|
-
else
|
|
1682
|
-
|
|
1683
|
-
atEnd) {
|
|
1684
|
-
target = rows[rIdx + 1].children[cIdx];
|
|
2256
|
+
else {
|
|
2257
|
+
document.execCommand("insertText", false, " ");
|
|
1685
2258
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
2259
|
+
}
|
|
2260
|
+
// Table navigation with arrows inside cells
|
|
2261
|
+
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
|
|
2262
|
+
const sel = window.getSelection();
|
|
2263
|
+
const cell = getClosestCell(sel?.anchorNode || null);
|
|
2264
|
+
if (table &&
|
|
2265
|
+
cell &&
|
|
2266
|
+
cell.parentElement &&
|
|
2267
|
+
cell.parentElement.parentElement) {
|
|
2268
|
+
const row = cell.parentElement;
|
|
2269
|
+
const tbody = row.parentElement;
|
|
2270
|
+
const cells = Array.from(row.children).filter((c) => c.tagName === "TD" ||
|
|
2271
|
+
c.tagName === "TH");
|
|
2272
|
+
const rows = Array.from(tbody.children);
|
|
2273
|
+
const rIdx = rows.indexOf(row);
|
|
2274
|
+
const cIdx = cells.indexOf(cell);
|
|
2275
|
+
const atStart = (sel?.anchorOffset || 0) === 0;
|
|
2276
|
+
const cellTextLen = (cell.textContent || "").length;
|
|
2277
|
+
const atEnd = (sel?.anchorOffset || 0) >= cellTextLen;
|
|
2278
|
+
let target = null;
|
|
2279
|
+
if (e.key === "ArrowLeft" && atStart && cIdx > 0) {
|
|
2280
|
+
target = row.children[cIdx - 1];
|
|
2281
|
+
}
|
|
2282
|
+
else if (e.key === "ArrowRight" &&
|
|
2283
|
+
atEnd &&
|
|
2284
|
+
cIdx < row.children.length - 1) {
|
|
2285
|
+
target = row.children[cIdx + 1];
|
|
2286
|
+
}
|
|
2287
|
+
else if (e.key === "ArrowUp" && rIdx > 0 && atStart) {
|
|
2288
|
+
target = rows[rIdx - 1].children[cIdx];
|
|
2289
|
+
}
|
|
2290
|
+
else if (e.key === "ArrowDown" &&
|
|
2291
|
+
rIdx < rows.length - 1 &&
|
|
2292
|
+
atEnd) {
|
|
2293
|
+
target = rows[rIdx + 1].children[cIdx];
|
|
2294
|
+
}
|
|
2295
|
+
if (target) {
|
|
2296
|
+
e.preventDefault();
|
|
2297
|
+
moveCaretToCell(target, e.key === "ArrowRight" || e.key === "ArrowDown");
|
|
2298
|
+
}
|
|
1689
2299
|
}
|
|
1690
2300
|
}
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
clearSelectionDecor();
|
|
1696
|
-
return;
|
|
1697
|
-
}
|
|
1698
|
-
const pos = getCellPosition(cell);
|
|
1699
|
-
if (!pos)
|
|
1700
|
-
return;
|
|
1701
|
-
selectingRef.current = { tbody: pos.tbody, start: cell };
|
|
1702
|
-
const onMove = (ev) => {
|
|
1703
|
-
const under = document.elementFromPoint(ev.clientX, ev.clientY);
|
|
1704
|
-
const overCell = getClosestCell(under);
|
|
1705
|
-
const startInfo = selectingRef.current;
|
|
1706
|
-
if (!overCell || !startInfo)
|
|
2301
|
+
}, onMouseDown: (e) => {
|
|
2302
|
+
const cell = getClosestCell(e.target);
|
|
2303
|
+
if (!cell) {
|
|
2304
|
+
clearSelectionDecor();
|
|
1707
2305
|
return;
|
|
1708
|
-
|
|
1709
|
-
const
|
|
1710
|
-
if (!
|
|
2306
|
+
}
|
|
2307
|
+
const pos = getCellPosition(cell);
|
|
2308
|
+
if (!pos)
|
|
1711
2309
|
return;
|
|
1712
|
-
|
|
1713
|
-
const
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
const
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
2310
|
+
selectingRef.current = { tbody: pos.tbody, start: cell };
|
|
2311
|
+
const onMove = (ev) => {
|
|
2312
|
+
const under = document.elementFromPoint(ev.clientX, ev.clientY);
|
|
2313
|
+
const overCell = getClosestCell(under);
|
|
2314
|
+
const startInfo = selectingRef.current;
|
|
2315
|
+
if (!overCell || !startInfo)
|
|
2316
|
+
return;
|
|
2317
|
+
const a = getCellPosition(startInfo.start);
|
|
2318
|
+
const b = getCellPosition(overCell);
|
|
2319
|
+
if (!a || !b || a.tbody !== b.tbody)
|
|
2320
|
+
return;
|
|
2321
|
+
const sr = Math.min(a.rIdx, b.rIdx);
|
|
2322
|
+
const sc = Math.min(a.cIdx, b.cIdx);
|
|
2323
|
+
const er = Math.max(a.rIdx, b.rIdx);
|
|
2324
|
+
const ec = Math.max(a.cIdx, b.cIdx);
|
|
2325
|
+
updateSelectionDecor(a.tbody, sr, sc, er, ec);
|
|
2326
|
+
};
|
|
2327
|
+
const onUp = () => {
|
|
2328
|
+
window.removeEventListener("mousemove", onMove);
|
|
2329
|
+
window.removeEventListener("mouseup", onUp);
|
|
2330
|
+
selectingRef.current = null;
|
|
2331
|
+
};
|
|
2332
|
+
window.addEventListener("mousemove", onMove);
|
|
2333
|
+
window.addEventListener("mouseup", onUp);
|
|
2334
|
+
}, onContextMenu: (e) => {
|
|
2335
|
+
const target = e.target;
|
|
2336
|
+
if (target && target.tagName === "IMG") {
|
|
2337
|
+
e.preventDefault();
|
|
2338
|
+
const vw = window.innerWidth;
|
|
2339
|
+
const vh = window.innerHeight;
|
|
2340
|
+
const menuW = 220;
|
|
2341
|
+
const menuH = 200;
|
|
2342
|
+
const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
|
|
2343
|
+
const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
|
|
2344
|
+
setImageMenu({ x, y, img: target });
|
|
2345
|
+
setTableMenu(null);
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
const cell = getClosestCell(e.target);
|
|
2349
|
+
if (cell) {
|
|
2350
|
+
e.preventDefault();
|
|
2351
|
+
const vw = window.innerWidth;
|
|
2352
|
+
const vh = window.innerHeight;
|
|
2353
|
+
const menuW = 220;
|
|
2354
|
+
const menuH = 300;
|
|
2355
|
+
const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
|
|
2356
|
+
const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
|
|
2357
|
+
setTableMenu({ x, y, cell });
|
|
2358
|
+
}
|
|
2359
|
+
else {
|
|
2360
|
+
setTableMenu(null);
|
|
2361
|
+
setImageMenu(null);
|
|
2362
|
+
}
|
|
2363
|
+
} }) }), selectedImage && imageOverlay && (_jsxs("div", { style: {
|
|
1755
2364
|
position: "fixed",
|
|
1756
2365
|
left: imageOverlay.left,
|
|
1757
2366
|
top: imageOverlay.top,
|