smartrte-react 0.1.13 → 0.1.14
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.
|
@@ -10,6 +10,21 @@ type ClassicEditorProps = {
|
|
|
10
10
|
media?: boolean;
|
|
11
11
|
formula?: boolean;
|
|
12
12
|
mediaManager?: MediaManagerAdapter;
|
|
13
|
+
/**
|
|
14
|
+
* Optional custom list of fonts to display in the toolbar.
|
|
15
|
+
* If not provided, a default set of web-safe fonts will be used.
|
|
16
|
+
* Example: [{ name: 'Robto', value: 'Roboto, sans-serif' }]
|
|
17
|
+
*/
|
|
18
|
+
fonts?: {
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}[];
|
|
22
|
+
/**
|
|
23
|
+
* The default font family to apply to the editor content.
|
|
24
|
+
* This sets the font-family style of the editable area.
|
|
25
|
+
* Example: "Arial, sans-serif"
|
|
26
|
+
*/
|
|
27
|
+
defaultFont?: string;
|
|
13
28
|
};
|
|
14
|
-
export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, table, media, formula, mediaManager, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
29
|
+
export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, table, media, formula, mediaManager, fonts, defaultFont, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
15
30
|
export {};
|
|
@@ -1,7 +1,15 @@
|
|
|
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
|
+
export function ClassicEditor({ value, onChange, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, table = true, media = true, formula = true, mediaManager, fonts = [
|
|
5
|
+
{ name: "Arial", value: "Arial, Helvetica, sans-serif" },
|
|
6
|
+
{ name: "Georgia", value: "Georgia, serif" },
|
|
7
|
+
{ name: "Impact", value: "Impact, Charcoal, sans-serif" },
|
|
8
|
+
{ name: "Tahoma", value: "Tahoma, Geneva, sans-serif" },
|
|
9
|
+
{ name: "Times New Roman", value: "'Times New Roman', Times, serif" },
|
|
10
|
+
{ name: "Verdana", value: "Verdana, Geneva, sans-serif" },
|
|
11
|
+
{ name: "Courier New", value: "'Courier New', Courier, monospace" },
|
|
12
|
+
], defaultFont, }) {
|
|
5
13
|
const editableRef = useRef(null);
|
|
6
14
|
const lastEmittedRef = useRef("");
|
|
7
15
|
const isComposingRef = useRef(false);
|
|
@@ -25,7 +33,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
25
33
|
const [showColorPicker, setShowColorPicker] = useState(false);
|
|
26
34
|
const [colorPickerType, setColorPickerType] = useState('text');
|
|
27
35
|
const savedRangeRef = useRef(null);
|
|
28
|
-
const [currentFontSize, setCurrentFontSize] = useState("
|
|
36
|
+
const [currentFontSize, setCurrentFontSize] = useState("");
|
|
37
|
+
const [currentFont, setCurrentFont] = useState("");
|
|
29
38
|
useEffect(() => {
|
|
30
39
|
const el = editableRef.current;
|
|
31
40
|
if (!el)
|
|
@@ -33,6 +42,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
33
42
|
// Initialize with provided HTML only when externally controlled value changes
|
|
34
43
|
if (typeof value === "string" && value !== el.innerHTML) {
|
|
35
44
|
el.innerHTML = value || "";
|
|
45
|
+
fixNegativeMargins(el);
|
|
46
|
+
ensureTableWrappers(el);
|
|
36
47
|
}
|
|
37
48
|
// Suppress native context menu inside table cells at capture phase
|
|
38
49
|
const onCtx = (evt) => {
|
|
@@ -152,6 +163,57 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
152
163
|
console.error('Error applying font size:', error);
|
|
153
164
|
}
|
|
154
165
|
};
|
|
166
|
+
const applyFontFamily = (font) => {
|
|
167
|
+
try {
|
|
168
|
+
setCurrentFont(font);
|
|
169
|
+
const editor = editableRef.current;
|
|
170
|
+
if (!editor)
|
|
171
|
+
return;
|
|
172
|
+
editor.focus();
|
|
173
|
+
let range = null;
|
|
174
|
+
const sel = window.getSelection();
|
|
175
|
+
if (sel && sel.rangeCount > 0) {
|
|
176
|
+
const currentRange = sel.getRangeAt(0);
|
|
177
|
+
if (editor.contains(currentRange.commonAncestorContainer)) {
|
|
178
|
+
range = currentRange;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!range && savedRangeRef.current) {
|
|
182
|
+
range = savedRangeRef.current.cloneRange();
|
|
183
|
+
}
|
|
184
|
+
if (!range)
|
|
185
|
+
return;
|
|
186
|
+
if (range.collapsed) {
|
|
187
|
+
const span = document.createElement('span');
|
|
188
|
+
span.style.fontFamily = font;
|
|
189
|
+
span.textContent = '\u200B';
|
|
190
|
+
range.insertNode(span);
|
|
191
|
+
const newRange = document.createRange();
|
|
192
|
+
newRange.setStart(span.firstChild, 1);
|
|
193
|
+
newRange.collapse(true);
|
|
194
|
+
if (sel) {
|
|
195
|
+
sel.removeAllRanges();
|
|
196
|
+
sel.addRange(newRange);
|
|
197
|
+
}
|
|
198
|
+
handleInput();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const span = document.createElement('span');
|
|
202
|
+
span.style.fontFamily = font;
|
|
203
|
+
const fragment = range.extractContents();
|
|
204
|
+
span.appendChild(fragment);
|
|
205
|
+
range.insertNode(span);
|
|
206
|
+
if (sel) {
|
|
207
|
+
range.selectNodeContents(span);
|
|
208
|
+
sel.removeAllRanges();
|
|
209
|
+
sel.addRange(range);
|
|
210
|
+
}
|
|
211
|
+
handleInput();
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
console.error('Error applying font family:', error);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
155
217
|
const applyTextColor = (color) => {
|
|
156
218
|
exec("foreColor", color);
|
|
157
219
|
};
|
|
@@ -466,11 +528,61 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
466
528
|
});
|
|
467
529
|
}
|
|
468
530
|
};
|
|
531
|
+
const fixNegativeMargins = (root) => {
|
|
532
|
+
try {
|
|
533
|
+
const nodes = root.querySelectorAll('*');
|
|
534
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
535
|
+
const node = nodes[i];
|
|
536
|
+
if (node.style && node.style.marginLeft && node.style.marginLeft.trim().startsWith('-')) {
|
|
537
|
+
node.style.marginLeft = '0px';
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch { }
|
|
542
|
+
};
|
|
543
|
+
const ensureTableWrappers = (root) => {
|
|
544
|
+
try {
|
|
545
|
+
const tables = root.querySelectorAll('table');
|
|
546
|
+
tables.forEach((table) => {
|
|
547
|
+
const parent = table.parentElement;
|
|
548
|
+
if (parent && parent.getAttribute('data-table-wrapper') !== 'true') {
|
|
549
|
+
const wrapper = document.createElement('div');
|
|
550
|
+
wrapper.setAttribute('data-table-wrapper', 'true');
|
|
551
|
+
wrapper.style.overflowX = 'auto';
|
|
552
|
+
wrapper.style.webkitOverflowScrolling = 'touch';
|
|
553
|
+
wrapper.style.width = '100%';
|
|
554
|
+
wrapper.style.maxWidth = '100%';
|
|
555
|
+
wrapper.style.display = 'block';
|
|
556
|
+
// Use insertBefore + appendChild to move element without losing too much state
|
|
557
|
+
// simpler than replaceChild for wrapping
|
|
558
|
+
parent.insertBefore(wrapper, table);
|
|
559
|
+
wrapper.appendChild(table);
|
|
560
|
+
}
|
|
561
|
+
// Always ensure table takes full width
|
|
562
|
+
if (table.style.width !== '100%') {
|
|
563
|
+
table.style.width = '100%';
|
|
564
|
+
}
|
|
565
|
+
// Ensure min-width is set
|
|
566
|
+
if (!table.style.minWidth || table.style.minWidth === '0px') {
|
|
567
|
+
table.style.minWidth = '100%';
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
catch (e) {
|
|
572
|
+
console.error("Error wrapping tables", e);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
469
575
|
const handleInput = () => {
|
|
470
576
|
if (isComposingRef.current)
|
|
471
577
|
return;
|
|
472
578
|
const el = editableRef.current;
|
|
473
|
-
if (!el
|
|
579
|
+
if (!el)
|
|
580
|
+
return;
|
|
581
|
+
// Auto-fix negative margins that might cause visibility issues
|
|
582
|
+
fixNegativeMargins(el);
|
|
583
|
+
// Ensure tables are wrapped for horizontal scrolling
|
|
584
|
+
ensureTableWrappers(el);
|
|
585
|
+
if (!onChange)
|
|
474
586
|
return;
|
|
475
587
|
const html = el.innerHTML;
|
|
476
588
|
if (html !== lastEmittedRef.current) {
|
|
@@ -481,7 +593,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
481
593
|
const buildTableHTML = (rows, cols) => {
|
|
482
594
|
const safeRows = Math.max(1, Math.min(50, Math.floor(rows) || 1));
|
|
483
595
|
const safeCols = Math.max(1, Math.min(20, Math.floor(cols) || 1));
|
|
484
|
-
let html = '<table style="border-collapse:collapse;width:100%;"><tbody>';
|
|
596
|
+
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
597
|
for (let r = 0; r < safeRows; r++) {
|
|
486
598
|
html += "<tr>";
|
|
487
599
|
for (let c = 0; c < safeCols; c++) {
|
|
@@ -490,7 +602,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
490
602
|
}
|
|
491
603
|
html += "</tr>";
|
|
492
604
|
}
|
|
493
|
-
html += "</tbody></table>";
|
|
605
|
+
html += "</tbody></table></div>";
|
|
494
606
|
return html;
|
|
495
607
|
};
|
|
496
608
|
const insertTable = () => {
|
|
@@ -895,6 +1007,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
895
1007
|
return;
|
|
896
1008
|
const firstCell = cells[0];
|
|
897
1009
|
const currentWidth = firstCell.offsetWidth;
|
|
1010
|
+
// Unlock table width so it can grow
|
|
1011
|
+
table.style.width = "max-content";
|
|
1012
|
+
table.style.minWidth = "100%";
|
|
898
1013
|
tableResizeRef.current = {
|
|
899
1014
|
type: 'column',
|
|
900
1015
|
table,
|
|
@@ -991,14 +1106,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
991
1106
|
});
|
|
992
1107
|
});
|
|
993
1108
|
};
|
|
994
|
-
return (_jsxs("div", { style: {
|
|
1109
|
+
return (_jsxs("div", { style: {
|
|
1110
|
+
border: "1px solid #ddd",
|
|
1111
|
+
borderRadius: 6,
|
|
1112
|
+
width: "100%",
|
|
1113
|
+
maxWidth: "100vw",
|
|
1114
|
+
overflow: "hidden",
|
|
1115
|
+
display: "flex",
|
|
1116
|
+
flexDirection: "column",
|
|
1117
|
+
background: "#fff",
|
|
1118
|
+
boxSizing: "border-box"
|
|
1119
|
+
}, children: [_jsxs("div", { style: {
|
|
995
1120
|
display: "flex",
|
|
996
1121
|
flexWrap: "wrap",
|
|
1122
|
+
maxWidth: "100%",
|
|
997
1123
|
gap: 8,
|
|
998
1124
|
padding: 8,
|
|
999
1125
|
borderBottom: "1px solid #eee",
|
|
1000
|
-
background: "#fff",
|
|
1001
|
-
color: "#111",
|
|
1002
1126
|
position: "sticky",
|
|
1003
1127
|
top: 0,
|
|
1004
1128
|
zIndex: 1,
|
|
@@ -1097,7 +1221,24 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1097
1221
|
borderRadius: 6,
|
|
1098
1222
|
background: "#fff",
|
|
1099
1223
|
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("
|
|
1224
|
+
}, 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: () => {
|
|
1225
|
+
const sel = window.getSelection();
|
|
1226
|
+
if (sel && sel.rangeCount > 0) {
|
|
1227
|
+
const range = sel.getRangeAt(0);
|
|
1228
|
+
const editor = editableRef.current;
|
|
1229
|
+
if (editor && editor.contains(range.commonAncestorContainer) && !range.collapsed) {
|
|
1230
|
+
savedRangeRef.current = range.cloneRange();
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}, onChange: (e) => applyFontFamily(e.target.value), title: "Font Family", style: {
|
|
1234
|
+
height: 32,
|
|
1235
|
+
padding: "0 8px",
|
|
1236
|
+
border: "1px solid #e5e7eb",
|
|
1237
|
+
borderRadius: 6,
|
|
1238
|
+
background: "#fff",
|
|
1239
|
+
color: "#111",
|
|
1240
|
+
maxWidth: 100,
|
|
1241
|
+
}, 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
1242
|
setColorPickerType('text');
|
|
1102
1243
|
setShowColorPicker(true);
|
|
1103
1244
|
}, style: {
|
|
@@ -1109,16 +1250,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1109
1250
|
background: "#fff",
|
|
1110
1251
|
color: "#111",
|
|
1111
1252
|
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: () => {
|
|
1253
|
+
}, children: _jsx("span", { style: { fontWeight: 700 }, children: "A" }) }), _jsx("button", { title: "Background Color", onClick: () => {
|
|
1122
1254
|
setColorPickerType('background');
|
|
1123
1255
|
setShowColorPicker(true);
|
|
1124
1256
|
}, style: {
|
|
@@ -1129,7 +1261,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1129
1261
|
borderRadius: 6,
|
|
1130
1262
|
background: "#fff",
|
|
1131
1263
|
color: "#111",
|
|
1132
|
-
}, children: _jsx("span", { style: { fontWeight: 700,
|
|
1264
|
+
}, children: _jsx("span", { style: { fontWeight: 700, padding: "2px 4px" }, children: "A" }) }), _jsx("button", { title: "Bulleted list", onClick: () => exec("insertUnorderedList"), style: {
|
|
1133
1265
|
height: 32,
|
|
1134
1266
|
padding: "0 10px",
|
|
1135
1267
|
border: "1px solid #e5e7eb",
|
|
@@ -1332,6 +1464,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1332
1464
|
padding: 16,
|
|
1333
1465
|
borderRadius: 8,
|
|
1334
1466
|
minWidth: 320,
|
|
1467
|
+
maxWidth: "90vw",
|
|
1335
1468
|
color: "#000",
|
|
1336
1469
|
}, 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
1470
|
display: "grid",
|
|
@@ -1485,273 +1618,285 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1485
1618
|
borderRadius: 4,
|
|
1486
1619
|
background: "#fff",
|
|
1487
1620
|
color: "#000",
|
|
1488
|
-
}, title: sym, children: sym }, i)))] })] }) })), _jsx("div", {
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1621
|
+
}, title: sym, children: sym }, i)))] })] }) })), _jsx("div", { style: {
|
|
1622
|
+
width: "100%",
|
|
1623
|
+
maxWidth: "100%",
|
|
1624
|
+
flex: "1 1 auto",
|
|
1625
|
+
minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
|
|
1626
|
+
maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
|
|
1627
|
+
overflowY: "auto",
|
|
1628
|
+
overflowX: "hidden",
|
|
1629
|
+
boxSizing: "border-box",
|
|
1630
|
+
position: "relative",
|
|
1631
|
+
}, children: _jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onInput: handleInput, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
|
|
1632
|
+
isComposingRef.current = false;
|
|
1633
|
+
handleInput();
|
|
1634
|
+
}, onPaste: (e) => {
|
|
1635
|
+
const items = e.clipboardData?.files;
|
|
1636
|
+
if (media && items && items.length) {
|
|
1637
|
+
const hasImage = Array.from(items).some((f) => f.type.startsWith("image/"));
|
|
1638
|
+
if (hasImage) {
|
|
1639
|
+
e.preventDefault();
|
|
1640
|
+
handleLocalImageFiles(items);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}, onDragOver: (e) => {
|
|
1644
|
+
// Allow dragging images within editor and file drops
|
|
1645
|
+
if (draggedImageRef.current ||
|
|
1646
|
+
e.dataTransfer?.types?.includes("Files")) {
|
|
1496
1647
|
e.preventDefault();
|
|
1497
|
-
handleLocalImageFiles(items);
|
|
1498
1648
|
}
|
|
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) {
|
|
1649
|
+
}, onDrop: (e) => {
|
|
1650
|
+
// Move existing dragged image inside editor
|
|
1651
|
+
if (draggedImageRef.current) {
|
|
1652
|
+
e.preventDefault();
|
|
1653
|
+
const x = e.clientX;
|
|
1654
|
+
const y = e.clientY;
|
|
1655
|
+
let range = null;
|
|
1515
1656
|
// @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);
|
|
1657
|
+
if (document.caretRangeFromPoint) {
|
|
1658
|
+
// @ts-ignore
|
|
1659
|
+
range = document.caretRangeFromPoint(x, y);
|
|
1523
1660
|
}
|
|
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;
|
|
1661
|
+
else if (document.caretPositionFromPoint) {
|
|
1662
|
+
const pos = document.caretPositionFromPoint(x, y);
|
|
1663
|
+
if (pos) {
|
|
1664
|
+
range = document.createRange();
|
|
1665
|
+
range.setStart(pos.offsetNode, pos.offset);
|
|
1541
1666
|
}
|
|
1542
|
-
el = el.parentElement;
|
|
1543
1667
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
range.
|
|
1668
|
+
const img = draggedImageRef.current;
|
|
1669
|
+
draggedImageRef.current = null;
|
|
1670
|
+
if (range &&
|
|
1671
|
+
img &&
|
|
1672
|
+
editableRef.current?.contains(range.commonAncestorContainer)) {
|
|
1673
|
+
// Avoid inserting inside the image itself
|
|
1674
|
+
if (range.startContainer === img || range.endContainer === img)
|
|
1675
|
+
return;
|
|
1676
|
+
// If dropping inside a link, insert right after the link element
|
|
1677
|
+
let container = range.commonAncestorContainer;
|
|
1678
|
+
let linkAncestor = null;
|
|
1679
|
+
let el = container;
|
|
1680
|
+
while (el && el !== editableRef.current) {
|
|
1681
|
+
if (el.tagName === "A") {
|
|
1682
|
+
linkAncestor = el;
|
|
1683
|
+
break;
|
|
1684
|
+
}
|
|
1685
|
+
el = el.parentElement;
|
|
1686
|
+
}
|
|
1687
|
+
if (linkAncestor) {
|
|
1688
|
+
linkAncestor.parentElement?.insertBefore(img, linkAncestor.nextSibling);
|
|
1689
|
+
}
|
|
1690
|
+
else {
|
|
1691
|
+
range.insertNode(img);
|
|
1692
|
+
}
|
|
1693
|
+
const r = document.createRange();
|
|
1694
|
+
r.setStartAfter(img);
|
|
1695
|
+
r.collapse(true);
|
|
1696
|
+
safeSelectRange(r);
|
|
1697
|
+
setSelectedImage(img);
|
|
1698
|
+
scheduleImageOverlay();
|
|
1699
|
+
handleInput();
|
|
1549
1700
|
}
|
|
1550
|
-
|
|
1551
|
-
r.setStartAfter(img);
|
|
1552
|
-
r.collapse(true);
|
|
1553
|
-
safeSelectRange(r);
|
|
1554
|
-
setSelectedImage(img);
|
|
1555
|
-
scheduleImageOverlay();
|
|
1556
|
-
handleInput();
|
|
1701
|
+
return;
|
|
1557
1702
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
const y = e.clientY;
|
|
1565
|
-
let range = null;
|
|
1566
|
-
// @ts-ignore
|
|
1567
|
-
if (document.caretRangeFromPoint) {
|
|
1703
|
+
if (media && e.dataTransfer?.files?.length) {
|
|
1704
|
+
e.preventDefault();
|
|
1705
|
+
// Try to move caret to drop point
|
|
1706
|
+
const x = e.clientX;
|
|
1707
|
+
const y = e.clientY;
|
|
1708
|
+
let range = null;
|
|
1568
1709
|
// @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);
|
|
1710
|
+
if (document.caretRangeFromPoint) {
|
|
1711
|
+
// @ts-ignore
|
|
1712
|
+
range = document.caretRangeFromPoint(x, y);
|
|
1576
1713
|
}
|
|
1714
|
+
else if (document.caretPositionFromPoint) {
|
|
1715
|
+
const pos = document.caretPositionFromPoint(x, y);
|
|
1716
|
+
if (pos) {
|
|
1717
|
+
range = document.createRange();
|
|
1718
|
+
range.setStart(pos.offsetNode, pos.offset);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (range) {
|
|
1722
|
+
const sel = window.getSelection();
|
|
1723
|
+
sel?.removeAllRanges();
|
|
1724
|
+
sel?.addRange(range);
|
|
1725
|
+
}
|
|
1726
|
+
handleLocalImageFiles(e.dataTransfer.files);
|
|
1577
1727
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1728
|
+
}, onClick: (e) => {
|
|
1729
|
+
const t = e.target;
|
|
1730
|
+
if (t && t.tagName === "IMG") {
|
|
1731
|
+
setSelectedImage(t);
|
|
1732
|
+
scheduleImageOverlay();
|
|
1582
1733
|
}
|
|
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);
|
|
1734
|
+
else {
|
|
1735
|
+
setSelectedImage(null);
|
|
1736
|
+
setImageOverlay(null);
|
|
1737
|
+
}
|
|
1738
|
+
}, onDragStart: (e) => {
|
|
1739
|
+
const t = e.target;
|
|
1740
|
+
if (t && t.tagName === "IMG") {
|
|
1741
|
+
draggedImageRef.current = t;
|
|
1742
|
+
try {
|
|
1743
|
+
e.dataTransfer?.setData("text/plain", "moving-image");
|
|
1744
|
+
e.dataTransfer.effectAllowed = "move";
|
|
1745
|
+
// Provide a subtle drag image
|
|
1746
|
+
const dt = e.dataTransfer;
|
|
1747
|
+
if (dt && typeof dt.setDragImage === "function") {
|
|
1748
|
+
const ghost = new Image();
|
|
1749
|
+
ghost.src = t.src;
|
|
1750
|
+
ghost.width = Math.min(120, t.width);
|
|
1751
|
+
ghost.height = Math.min(120, t.height);
|
|
1752
|
+
dt.setDragImage(ghost, 10, 10);
|
|
1753
|
+
}
|
|
1610
1754
|
}
|
|
1755
|
+
catch { }
|
|
1611
1756
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1757
|
+
else {
|
|
1758
|
+
draggedImageRef.current = null;
|
|
1759
|
+
}
|
|
1760
|
+
}, onDragEnd: () => {
|
|
1615
1761
|
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");
|
|
1762
|
+
}, style: {
|
|
1763
|
+
minHeight: "100%",
|
|
1764
|
+
maxWidth: "100%",
|
|
1765
|
+
overflowX: "hidden",
|
|
1766
|
+
padding: "16px",
|
|
1767
|
+
outline: "none",
|
|
1768
|
+
lineHeight: 1.6,
|
|
1769
|
+
boxSizing: "border-box",
|
|
1770
|
+
fontFamily: defaultFont || "inherit",
|
|
1771
|
+
}, "data-placeholder": placeholder, onFocus: (e) => {
|
|
1772
|
+
// Ensure the editor has at least one paragraph to type into
|
|
1773
|
+
const el = e.currentTarget;
|
|
1774
|
+
if (!el.innerHTML || el.innerHTML === "<br>") {
|
|
1775
|
+
el.innerHTML = "<p><br></p>";
|
|
1646
1776
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1777
|
+
}, onKeyDown: (e) => {
|
|
1778
|
+
if (formula &&
|
|
1779
|
+
(e.metaKey || e.ctrlKey) &&
|
|
1780
|
+
String(e.key).toLowerCase() === "m") {
|
|
1781
|
+
e.preventDefault();
|
|
1782
|
+
setShowFormulaDialog(true);
|
|
1783
|
+
return;
|
|
1649
1784
|
}
|
|
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];
|
|
1785
|
+
// Keep Tab for indentation in lists; otherwise insert 2 spaces
|
|
1786
|
+
if (e.key === "Tab") {
|
|
1787
|
+
e.preventDefault();
|
|
1788
|
+
if (document.queryCommandState("insertUnorderedList") ||
|
|
1789
|
+
document.queryCommandState("insertOrderedList")) {
|
|
1790
|
+
exec(e.shiftKey ? "outdent" : "indent");
|
|
1680
1791
|
}
|
|
1681
|
-
else
|
|
1682
|
-
|
|
1683
|
-
atEnd) {
|
|
1684
|
-
target = rows[rIdx + 1].children[cIdx];
|
|
1792
|
+
else {
|
|
1793
|
+
document.execCommand("insertText", false, " ");
|
|
1685
1794
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1795
|
+
}
|
|
1796
|
+
// Table navigation with arrows inside cells
|
|
1797
|
+
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
|
|
1798
|
+
const sel = window.getSelection();
|
|
1799
|
+
const cell = getClosestCell(sel?.anchorNode || null);
|
|
1800
|
+
if (table &&
|
|
1801
|
+
cell &&
|
|
1802
|
+
cell.parentElement &&
|
|
1803
|
+
cell.parentElement.parentElement) {
|
|
1804
|
+
const row = cell.parentElement;
|
|
1805
|
+
const tbody = row.parentElement;
|
|
1806
|
+
const cells = Array.from(row.children).filter((c) => c.tagName === "TD" ||
|
|
1807
|
+
c.tagName === "TH");
|
|
1808
|
+
const rows = Array.from(tbody.children);
|
|
1809
|
+
const rIdx = rows.indexOf(row);
|
|
1810
|
+
const cIdx = cells.indexOf(cell);
|
|
1811
|
+
const atStart = (sel?.anchorOffset || 0) === 0;
|
|
1812
|
+
const cellTextLen = (cell.textContent || "").length;
|
|
1813
|
+
const atEnd = (sel?.anchorOffset || 0) >= cellTextLen;
|
|
1814
|
+
let target = null;
|
|
1815
|
+
if (e.key === "ArrowLeft" && atStart && cIdx > 0) {
|
|
1816
|
+
target = row.children[cIdx - 1];
|
|
1817
|
+
}
|
|
1818
|
+
else if (e.key === "ArrowRight" &&
|
|
1819
|
+
atEnd &&
|
|
1820
|
+
cIdx < row.children.length - 1) {
|
|
1821
|
+
target = row.children[cIdx + 1];
|
|
1822
|
+
}
|
|
1823
|
+
else if (e.key === "ArrowUp" && rIdx > 0 && atStart) {
|
|
1824
|
+
target = rows[rIdx - 1].children[cIdx];
|
|
1825
|
+
}
|
|
1826
|
+
else if (e.key === "ArrowDown" &&
|
|
1827
|
+
rIdx < rows.length - 1 &&
|
|
1828
|
+
atEnd) {
|
|
1829
|
+
target = rows[rIdx + 1].children[cIdx];
|
|
1830
|
+
}
|
|
1831
|
+
if (target) {
|
|
1832
|
+
e.preventDefault();
|
|
1833
|
+
moveCaretToCell(target, e.key === "ArrowRight" || e.key === "ArrowDown");
|
|
1834
|
+
}
|
|
1689
1835
|
}
|
|
1690
1836
|
}
|
|
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)
|
|
1837
|
+
}, onMouseDown: (e) => {
|
|
1838
|
+
const cell = getClosestCell(e.target);
|
|
1839
|
+
if (!cell) {
|
|
1840
|
+
clearSelectionDecor();
|
|
1707
1841
|
return;
|
|
1708
|
-
|
|
1709
|
-
const
|
|
1710
|
-
if (!
|
|
1842
|
+
}
|
|
1843
|
+
const pos = getCellPosition(cell);
|
|
1844
|
+
if (!pos)
|
|
1711
1845
|
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
|
-
|
|
1846
|
+
selectingRef.current = { tbody: pos.tbody, start: cell };
|
|
1847
|
+
const onMove = (ev) => {
|
|
1848
|
+
const under = document.elementFromPoint(ev.clientX, ev.clientY);
|
|
1849
|
+
const overCell = getClosestCell(under);
|
|
1850
|
+
const startInfo = selectingRef.current;
|
|
1851
|
+
if (!overCell || !startInfo)
|
|
1852
|
+
return;
|
|
1853
|
+
const a = getCellPosition(startInfo.start);
|
|
1854
|
+
const b = getCellPosition(overCell);
|
|
1855
|
+
if (!a || !b || a.tbody !== b.tbody)
|
|
1856
|
+
return;
|
|
1857
|
+
const sr = Math.min(a.rIdx, b.rIdx);
|
|
1858
|
+
const sc = Math.min(a.cIdx, b.cIdx);
|
|
1859
|
+
const er = Math.max(a.rIdx, b.rIdx);
|
|
1860
|
+
const ec = Math.max(a.cIdx, b.cIdx);
|
|
1861
|
+
updateSelectionDecor(a.tbody, sr, sc, er, ec);
|
|
1862
|
+
};
|
|
1863
|
+
const onUp = () => {
|
|
1864
|
+
window.removeEventListener("mousemove", onMove);
|
|
1865
|
+
window.removeEventListener("mouseup", onUp);
|
|
1866
|
+
selectingRef.current = null;
|
|
1867
|
+
};
|
|
1868
|
+
window.addEventListener("mousemove", onMove);
|
|
1869
|
+
window.addEventListener("mouseup", onUp);
|
|
1870
|
+
}, onContextMenu: (e) => {
|
|
1871
|
+
const target = e.target;
|
|
1872
|
+
if (target && target.tagName === "IMG") {
|
|
1873
|
+
e.preventDefault();
|
|
1874
|
+
const vw = window.innerWidth;
|
|
1875
|
+
const vh = window.innerHeight;
|
|
1876
|
+
const menuW = 220;
|
|
1877
|
+
const menuH = 200;
|
|
1878
|
+
const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
|
|
1879
|
+
const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
|
|
1880
|
+
setImageMenu({ x, y, img: target });
|
|
1881
|
+
setTableMenu(null);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const cell = getClosestCell(e.target);
|
|
1885
|
+
if (cell) {
|
|
1886
|
+
e.preventDefault();
|
|
1887
|
+
const vw = window.innerWidth;
|
|
1888
|
+
const vh = window.innerHeight;
|
|
1889
|
+
const menuW = 220;
|
|
1890
|
+
const menuH = 300;
|
|
1891
|
+
const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
|
|
1892
|
+
const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
|
|
1893
|
+
setTableMenu({ x, y, cell });
|
|
1894
|
+
}
|
|
1895
|
+
else {
|
|
1896
|
+
setTableMenu(null);
|
|
1897
|
+
setImageMenu(null);
|
|
1898
|
+
}
|
|
1899
|
+
} }) }), selectedImage && imageOverlay && (_jsxs("div", { style: {
|
|
1755
1900
|
position: "fixed",
|
|
1756
1901
|
left: imageOverlay.left,
|
|
1757
1902
|
top: imageOverlay.top,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smartrte-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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",
|