smartrte-react 0.1.14 → 0.1.16
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/README.md +1 -1
- package/dist/components/ClassicEditor.js +489 -25
- package/package.json +25 -20
package/README.md
CHANGED
|
@@ -548,7 +548,7 @@ const ClassicEditor = dynamic(
|
|
|
548
548
|
|
|
549
549
|
### Reporting Security Issues
|
|
550
550
|
|
|
551
|
-
If you discover a security vulnerability, please email [
|
|
551
|
+
If you discover a security vulnerability, please email [support@openstash.in] instead of using the issue tracker.
|
|
552
552
|
|
|
553
553
|
### Content Sanitization
|
|
554
554
|
|
|
@@ -1,6 +1,12 @@
|
|
|
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
|
+
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
|
+
}
|
|
4
10
|
export function ClassicEditor({ value, onChange, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, table = true, media = true, formula = true, mediaManager, fonts = [
|
|
5
11
|
{ name: "Arial", value: "Arial, Helvetica, sans-serif" },
|
|
6
12
|
{ name: "Georgia", value: "Georgia, serif" },
|
|
@@ -14,6 +20,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
14
20
|
const lastEmittedRef = useRef("");
|
|
15
21
|
const isComposingRef = useRef(false);
|
|
16
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);
|
|
17
29
|
const replaceTargetRef = useRef(null);
|
|
18
30
|
const [selectedImage, setSelectedImage] = useState(null);
|
|
19
31
|
const [imageOverlay, setImageOverlay] = useState(null);
|
|
@@ -44,6 +56,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
44
56
|
el.innerHTML = value || "";
|
|
45
57
|
fixNegativeMargins(el);
|
|
46
58
|
ensureTableWrappers(el);
|
|
59
|
+
addTableResizeHandles();
|
|
47
60
|
}
|
|
48
61
|
// Suppress native context menu inside table cells at capture phase
|
|
49
62
|
const onCtx = (evt) => {
|
|
@@ -267,14 +280,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
267
280
|
if (!table)
|
|
268
281
|
return;
|
|
269
282
|
const target = e.target;
|
|
270
|
-
|
|
271
|
-
|
|
283
|
+
const cell = getClosestCell(target);
|
|
284
|
+
if (cell) {
|
|
285
|
+
const rect = cell.getBoundingClientRect();
|
|
272
286
|
const rightEdge = rect.right;
|
|
273
287
|
const clickX = e.clientX;
|
|
274
288
|
if (Math.abs(clickX - rightEdge) < 5) {
|
|
275
289
|
e.preventDefault();
|
|
276
|
-
const tableElem =
|
|
277
|
-
const colIndex = parseInt(
|
|
290
|
+
const tableElem = cell.closest('table');
|
|
291
|
+
const colIndex = parseInt(cell.getAttribute('data-col-index') || '0', 10);
|
|
278
292
|
if (tableElem) {
|
|
279
293
|
startColumnResize(tableElem, colIndex, e.clientX);
|
|
280
294
|
}
|
|
@@ -284,8 +298,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
284
298
|
const clickY = e.clientY;
|
|
285
299
|
if (Math.abs(clickY - bottomEdge) < 5) {
|
|
286
300
|
e.preventDefault();
|
|
287
|
-
const tableElem =
|
|
288
|
-
const row =
|
|
301
|
+
const tableElem = cell.closest('table');
|
|
302
|
+
const row = cell.closest('tr');
|
|
289
303
|
if (tableElem && row) {
|
|
290
304
|
const rowIndex = parseInt(row.getAttribute('data-row-index') || '0', 10);
|
|
291
305
|
startRowResize(tableElem, rowIndex, e.clientY);
|
|
@@ -301,23 +315,26 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
301
315
|
}
|
|
302
316
|
if (!table)
|
|
303
317
|
return;
|
|
318
|
+
let cursor = '';
|
|
304
319
|
const target = e.target;
|
|
305
|
-
|
|
306
|
-
|
|
320
|
+
const cell = getClosestCell(target);
|
|
321
|
+
if (cell) {
|
|
322
|
+
const rect = cell.getBoundingClientRect();
|
|
307
323
|
const clickX = e.clientX;
|
|
308
324
|
const clickY = e.clientY;
|
|
309
325
|
if (Math.abs(clickX - rect.right) < 5) {
|
|
310
|
-
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
if (Math.abs(clickY - rect.bottom) < 5) {
|
|
314
|
-
el.style.cursor = 'row-resize';
|
|
315
|
-
return;
|
|
326
|
+
cursor = 'col-resize';
|
|
316
327
|
}
|
|
317
|
-
if (
|
|
318
|
-
|
|
328
|
+
else if (Math.abs(clickY - rect.bottom) < 5) {
|
|
329
|
+
cursor = 'row-resize';
|
|
319
330
|
}
|
|
320
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
|
+
}
|
|
321
338
|
};
|
|
322
339
|
const onMouseUp = () => {
|
|
323
340
|
handleTableResizeEnd();
|
|
@@ -326,15 +343,16 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
326
343
|
if (!table)
|
|
327
344
|
return;
|
|
328
345
|
const target = e.target;
|
|
329
|
-
|
|
330
|
-
|
|
346
|
+
const cell = getClosestCell(target);
|
|
347
|
+
if (cell) {
|
|
348
|
+
const rect = cell.getBoundingClientRect();
|
|
331
349
|
const touch = e.touches[0];
|
|
332
350
|
const clickX = touch.clientX;
|
|
333
351
|
const clickY = touch.clientY;
|
|
334
352
|
if (Math.abs(clickX - rect.right) < 15) {
|
|
335
353
|
e.preventDefault();
|
|
336
|
-
const tableElem =
|
|
337
|
-
const colIndex = parseInt(
|
|
354
|
+
const tableElem = cell.closest('table');
|
|
355
|
+
const colIndex = parseInt(cell.getAttribute('data-col-index') || '0', 10);
|
|
338
356
|
if (tableElem) {
|
|
339
357
|
startColumnResize(tableElem, colIndex, clickX);
|
|
340
358
|
}
|
|
@@ -342,8 +360,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
342
360
|
}
|
|
343
361
|
if (Math.abs(clickY - rect.bottom) < 15) {
|
|
344
362
|
e.preventDefault();
|
|
345
|
-
const tableElem =
|
|
346
|
-
const row =
|
|
363
|
+
const tableElem = cell.closest('table');
|
|
364
|
+
const row = cell.closest('tr');
|
|
347
365
|
if (tableElem && row) {
|
|
348
366
|
const rowIndex = parseInt(row.getAttribute('data-row-index') || '0', 10);
|
|
349
367
|
startRowResize(tableElem, rowIndex, clickY);
|
|
@@ -528,6 +546,375 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
528
546
|
});
|
|
529
547
|
}
|
|
530
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
|
+
};
|
|
531
918
|
const fixNegativeMargins = (root) => {
|
|
532
919
|
try {
|
|
533
920
|
const nodes = root.querySelectorAll('*');
|
|
@@ -582,6 +969,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
582
969
|
fixNegativeMargins(el);
|
|
583
970
|
// Ensure tables are wrapped for horizontal scrolling
|
|
584
971
|
ensureTableWrappers(el);
|
|
972
|
+
// Add resize handles to tables
|
|
973
|
+
addTableResizeHandles();
|
|
585
974
|
if (!onChange)
|
|
586
975
|
return;
|
|
587
976
|
const html = el.innerHTML;
|
|
@@ -1152,7 +1541,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1152
1541
|
}
|
|
1153
1542
|
}
|
|
1154
1543
|
e.currentTarget.value = "";
|
|
1155
|
-
} })),
|
|
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) => {
|
|
1156
1551
|
const val = e.target.value;
|
|
1157
1552
|
if (val === "p")
|
|
1158
1553
|
applyFormatBlock("<p>");
|
|
@@ -1328,7 +1723,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1328
1723
|
borderRadius: 6,
|
|
1329
1724
|
background: "#fff",
|
|
1330
1725
|
color: "#111",
|
|
1331
|
-
}, 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: {
|
|
1332
1743
|
display: "inline-flex",
|
|
1333
1744
|
gap: 4,
|
|
1334
1745
|
alignItems: "center",
|
|
@@ -1451,7 +1862,60 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1451
1862
|
}, children: [_jsx("button", { onClick: () => setShowTableDialog(false), children: "Cancel" }), _jsx("button", { onClick: () => {
|
|
1452
1863
|
insertTable();
|
|
1453
1864
|
setShowTableDialog(false);
|
|
1454
|
-
}, 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: {
|
|
1455
1919
|
position: "fixed",
|
|
1456
1920
|
inset: 0,
|
|
1457
1921
|
background: "rgba(0,0,0,0.35)",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smartrte-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "A powerful, feature-rich Rich Text Editor for React with support for tables, mathematical formulas (LaTeX/KaTeX), and media management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -38,16 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"author": "Smart RTE Contributors",
|
|
40
40
|
"license": "MIT",
|
|
41
|
-
"
|
|
42
|
-
"build": "tsc -p tsconfig.json",
|
|
43
|
-
"prepublishOnly": "pnpm run build",
|
|
44
|
-
"dev": "pnpm build",
|
|
45
|
-
"lint": "eslint . || true",
|
|
46
|
-
"test": "vitest run || true",
|
|
47
|
-
"storybook": "storybook dev -p 6006",
|
|
48
|
-
"build-storybook": "storybook build",
|
|
49
|
-
"e2e": "playwright test || true"
|
|
50
|
-
},
|
|
41
|
+
"runkitExampleFilename": "runkit-example.js",
|
|
51
42
|
"publishConfig": {
|
|
52
43
|
"access": "public"
|
|
53
44
|
},
|
|
@@ -57,17 +48,31 @@
|
|
|
57
48
|
"react-dom": ">=18"
|
|
58
49
|
},
|
|
59
50
|
"devDependencies": {
|
|
60
|
-
"
|
|
61
|
-
"@
|
|
62
|
-
"@types/react-dom": "^18.3.0",
|
|
51
|
+
"@playwright/test": "^1.48.2",
|
|
52
|
+
"@storybook/addon-essentials": "^8.6.14",
|
|
63
53
|
"@storybook/react": "^8.6.14",
|
|
64
54
|
"@storybook/react-vite": "^8.6.14",
|
|
65
|
-
"@
|
|
55
|
+
"@types/pdfjs-dist": "^2.10.377",
|
|
56
|
+
"@types/react": "^18.3.4",
|
|
57
|
+
"@types/react-dom": "^18.3.0",
|
|
58
|
+
"react": "18.3.1",
|
|
59
|
+
"react-dom": "18.3.1",
|
|
66
60
|
"storybook": "^8.6.14",
|
|
61
|
+
"typescript": "^5.6.2",
|
|
67
62
|
"vite": "^5.4.8",
|
|
68
|
-
"vitest": "^2.1.4"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"
|
|
63
|
+
"vitest": "^2.1.4"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"mammoth": "^1.11.0",
|
|
67
|
+
"pdfjs-dist": "^5.4.530"
|
|
68
|
+
},
|
|
69
|
+
"scripts": {
|
|
70
|
+
"build": "tsc -p tsconfig.json",
|
|
71
|
+
"dev": "pnpm build",
|
|
72
|
+
"lint": "eslint . || true",
|
|
73
|
+
"test": "vitest run || true",
|
|
74
|
+
"storybook": "storybook dev -p 6006",
|
|
75
|
+
"build-storybook": "storybook build",
|
|
76
|
+
"e2e": "playwright test || true"
|
|
72
77
|
}
|
|
73
|
-
}
|
|
78
|
+
}
|