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.
@@ -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
- export function ClassicEditor({ value, onChange, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, table = true, media = true, formula = true, mediaManager, }) {
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("11");
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
- if (target.tagName === 'TD' || target.tagName === 'TH') {
209
- const rect = target.getBoundingClientRect();
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 = target.closest('table');
215
- const colIndex = parseInt(target.getAttribute('data-col-index') || '0', 10);
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 = target.closest('table');
226
- const row = target.closest('tr');
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
- if (target.tagName === 'TD' || target.tagName === 'TH') {
244
- const rect = target.getBoundingClientRect();
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
- el.style.cursor = 'col-resize';
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 (el.style.cursor === 'col-resize' || el.style.cursor === 'row-resize') {
256
- el.style.cursor = '';
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
- if (target.tagName === 'TD' || target.tagName === 'TH') {
268
- const rect = target.getBoundingClientRect();
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 = target.closest('table');
275
- const colIndex = parseInt(target.getAttribute('data-col-index') || '0', 10);
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 = target.closest('table');
284
- const row = target.closest('tr');
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 || '&nbsp;'}</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 || !onChange)
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: { border: "1px solid #ddd", borderRadius: 6 }, children: [_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
- } })), _jsxs("select", { defaultValue: "p", onChange: (e) => {
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("button", { title: "Text Color", onClick: () => {
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: [_jsx("span", { style: { fontWeight: 700 }, children: "A" }), _jsx("div", { style: {
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, background: "#ffeb3b", padding: "2px 4px" }, children: "A" }) }), _jsx("button", { title: "Bulleted list", onClick: () => exec("insertUnorderedList"), style: {
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" })), _jsxs("div", { style: {
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" })] })] }) })), showColorPicker && (_jsx("div", { style: {
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", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onInput: handleInput, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
1489
- isComposingRef.current = false;
1490
- handleInput();
1491
- }, onPaste: (e) => {
1492
- const items = e.clipboardData?.files;
1493
- if (media && items && items.length) {
1494
- const hasImage = Array.from(items).some((f) => f.type.startsWith("image/"));
1495
- if (hasImage) {
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
- }, onDragOver: (e) => {
1501
- // Allow dragging images within editor and file drops
1502
- if (draggedImageRef.current ||
1503
- e.dataTransfer?.types?.includes("Files")) {
1504
- e.preventDefault();
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
- range = document.caretRangeFromPoint(x, y);
1517
- }
1518
- else if (document.caretPositionFromPoint) {
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
- const img = draggedImageRef.current;
1526
- draggedImageRef.current = null;
1527
- if (range &&
1528
- img &&
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
- else {
1548
- range.insertNode(img);
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
- const r = document.createRange();
1551
- r.setStartAfter(img);
1552
- r.collapse(true);
1553
- safeSelectRange(r);
1554
- setSelectedImage(img);
1555
- scheduleImageOverlay();
1556
- handleInput();
2165
+ return;
1557
2166
  }
1558
- return;
1559
- }
1560
- if (media && e.dataTransfer?.files?.length) {
1561
- e.preventDefault();
1562
- // Try to move caret to drop point
1563
- const x = e.clientX;
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
- range = document.caretRangeFromPoint(x, y);
1570
- }
1571
- else if (document.caretPositionFromPoint) {
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
- if (range) {
1579
- const sel = window.getSelection();
1580
- sel?.removeAllRanges();
1581
- sel?.addRange(range);
2192
+ }, onClick: (e) => {
2193
+ const t = e.target;
2194
+ if (t && t.tagName === "IMG") {
2195
+ setSelectedImage(t);
2196
+ scheduleImageOverlay();
1582
2197
  }
1583
- handleLocalImageFiles(e.dataTransfer.files);
1584
- }
1585
- }, onClick: (e) => {
1586
- const t = e.target;
1587
- if (t && t.tagName === "IMG") {
1588
- setSelectedImage(t);
1589
- scheduleImageOverlay();
1590
- }
1591
- else {
1592
- setSelectedImage(null);
1593
- setImageOverlay(null);
1594
- }
1595
- }, onDragStart: (e) => {
1596
- const t = e.target;
1597
- if (t && t.tagName === "IMG") {
1598
- draggedImageRef.current = t;
1599
- try {
1600
- e.dataTransfer?.setData("text/plain", "moving-image");
1601
- e.dataTransfer.effectAllowed = "move";
1602
- // Provide a subtle drag image
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
- catch { }
1613
- }
1614
- else {
2221
+ else {
2222
+ draggedImageRef.current = null;
2223
+ }
2224
+ }, onDragEnd: () => {
1615
2225
  draggedImageRef.current = null;
1616
- }
1617
- }, onDragEnd: () => {
1618
- draggedImageRef.current = null;
1619
- }, style: {
1620
- padding: 12,
1621
- minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
1622
- maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
1623
- overflowY: "auto",
1624
- outline: "none",
1625
- lineHeight: 1.6,
1626
- }, "data-placeholder": placeholder, onFocus: (e) => {
1627
- // Ensure the editor has at least one paragraph to type into
1628
- const el = e.currentTarget;
1629
- if (!el.innerHTML || el.innerHTML === "<br>") {
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
- else {
1648
- document.execCommand("insertText", false, " ");
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
- // Table navigation with arrows inside cells
1652
- if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
1653
- const sel = window.getSelection();
1654
- const cell = getClosestCell(sel?.anchorNode || null);
1655
- if (table &&
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 if (e.key === "ArrowDown" &&
1682
- rIdx < rows.length - 1 &&
1683
- atEnd) {
1684
- target = rows[rIdx + 1].children[cIdx];
2256
+ else {
2257
+ document.execCommand("insertText", false, " ");
1685
2258
  }
1686
- if (target) {
1687
- e.preventDefault();
1688
- moveCaretToCell(target, e.key === "ArrowRight" || e.key === "ArrowDown");
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
- }, onMouseDown: (e) => {
1693
- const cell = getClosestCell(e.target);
1694
- if (!cell) {
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
- const a = getCellPosition(startInfo.start);
1709
- const b = getCellPosition(overCell);
1710
- if (!a || !b || a.tbody !== b.tbody)
2306
+ }
2307
+ const pos = getCellPosition(cell);
2308
+ if (!pos)
1711
2309
  return;
1712
- const sr = Math.min(a.rIdx, b.rIdx);
1713
- const sc = Math.min(a.cIdx, b.cIdx);
1714
- const er = Math.max(a.rIdx, b.rIdx);
1715
- const ec = Math.max(a.cIdx, b.cIdx);
1716
- updateSelectionDecor(a.tbody, sr, sc, er, ec);
1717
- };
1718
- const onUp = () => {
1719
- window.removeEventListener("mousemove", onMove);
1720
- window.removeEventListener("mouseup", onUp);
1721
- selectingRef.current = null;
1722
- };
1723
- window.addEventListener("mousemove", onMove);
1724
- window.addEventListener("mouseup", onUp);
1725
- }, onContextMenu: (e) => {
1726
- const target = e.target;
1727
- if (target && target.tagName === "IMG") {
1728
- e.preventDefault();
1729
- const vw = window.innerWidth;
1730
- const vh = window.innerHeight;
1731
- const menuW = 220;
1732
- const menuH = 200;
1733
- const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
1734
- const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
1735
- setImageMenu({ x, y, img: target });
1736
- setTableMenu(null);
1737
- return;
1738
- }
1739
- const cell = getClosestCell(e.target);
1740
- if (cell) {
1741
- e.preventDefault();
1742
- const vw = window.innerWidth;
1743
- const vh = window.innerHeight;
1744
- const menuW = 220;
1745
- const menuH = 300;
1746
- const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
1747
- const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
1748
- setTableMenu({ x, y, cell });
1749
- }
1750
- else {
1751
- setTableMenu(null);
1752
- setImageMenu(null);
1753
- }
1754
- } }), selectedImage && imageOverlay && (_jsxs("div", { style: {
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,