smartrte-react 0.2.5 → 0.2.7
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.
|
@@ -50,6 +50,11 @@ type ClassicEditorProps = {
|
|
|
50
50
|
* - "dark": Uses the built-in dark theme.
|
|
51
51
|
*/
|
|
52
52
|
theme?: SrteTheme;
|
|
53
|
+
/**
|
|
54
|
+
* Show the inline font-size dropdown in the toolbar.
|
|
55
|
+
* Defaults to true. Heading block controls are independent of this option.
|
|
56
|
+
*/
|
|
57
|
+
showFontSize?: boolean;
|
|
53
58
|
/**
|
|
54
59
|
* Additional CSS class name(s) to apply to the editor's root element.
|
|
55
60
|
* Useful for custom theming by overriding CSS custom properties.
|
|
@@ -57,5 +62,5 @@ type ClassicEditorProps = {
|
|
|
57
62
|
*/
|
|
58
63
|
className?: string;
|
|
59
64
|
};
|
|
60
|
-
export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, table, media, formula, mediaManager, fonts, defaultFont, preserveFontFamily, preserveColors, preserveDocxStyles, theme, className, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
65
|
+
export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, table, media, formula, mediaManager, fonts, defaultFont, preserveFontFamily, preserveColors, preserveDocxStyles, theme, showFontSize, className, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
61
66
|
export {};
|
|
@@ -17,7 +17,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
17
17
|
{ name: "Times New Roman", value: "'Times New Roman', Times, serif" },
|
|
18
18
|
{ name: "Verdana", value: "Verdana, Geneva, sans-serif" },
|
|
19
19
|
{ name: "Courier New", value: "'Courier New', Courier, monospace" },
|
|
20
|
-
], defaultFont, preserveFontFamily = false, preserveColors = false, preserveDocxStyles = true, theme = "light", className, }) {
|
|
20
|
+
], defaultFont, preserveFontFamily = false, preserveColors = false, preserveDocxStyles = true, theme = "light", showFontSize = true, className, }) {
|
|
21
21
|
ensureStyleSheet();
|
|
22
22
|
const editableRef = useRef(null);
|
|
23
23
|
const editorScrollRef = useRef(null);
|
|
@@ -37,6 +37,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
37
37
|
const [imageOverlay, setImageOverlay] = useState(null);
|
|
38
38
|
const resizingRef = useRef(null);
|
|
39
39
|
const draggedImageRef = useRef(null);
|
|
40
|
+
const draggedBlockRef = useRef(null);
|
|
41
|
+
const dragHandleHideTimerRef = useRef(null);
|
|
42
|
+
const [dragHandle, setDragHandle] = useState(null);
|
|
40
43
|
const tableResizeRef = useRef(null);
|
|
41
44
|
const [showTableDialog, setShowTableDialog] = useState(false);
|
|
42
45
|
const [tableRows, setTableRows] = useState(3);
|
|
@@ -52,8 +55,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
52
55
|
const [showSpecialChars, setShowSpecialChars] = useState(false);
|
|
53
56
|
const [colorPickerType, setColorPickerType] = useState('text');
|
|
54
57
|
const savedRangeRef = useRef(null);
|
|
58
|
+
const historyRef = useRef({
|
|
59
|
+
undo: [],
|
|
60
|
+
redo: [],
|
|
61
|
+
});
|
|
55
62
|
const [currentFontSize, setCurrentFontSize] = useState("");
|
|
56
63
|
const [currentFont, setCurrentFont] = useState("");
|
|
64
|
+
const [currentBlockType, setCurrentBlockType] = useState("p");
|
|
57
65
|
const [activeState, setActiveState] = useState({
|
|
58
66
|
bold: false,
|
|
59
67
|
italic: false,
|
|
@@ -104,6 +112,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
104
112
|
if (node.nodeType === Node.TEXT_NODE)
|
|
105
113
|
node = node.parentNode;
|
|
106
114
|
const element = node instanceof HTMLElement ? node : null;
|
|
115
|
+
const block = element?.closest("p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div");
|
|
116
|
+
const tag = block?.tagName.toLowerCase();
|
|
117
|
+
setCurrentBlockType(tag === "h1" || tag === "h2" || tag === "h3" ? tag : "p");
|
|
107
118
|
setActiveState({
|
|
108
119
|
bold: document.queryCommandState("bold"),
|
|
109
120
|
italic: document.queryCommandState("italic"),
|
|
@@ -139,6 +150,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
139
150
|
}, []);
|
|
140
151
|
const exec = (command, valueArg) => {
|
|
141
152
|
try {
|
|
153
|
+
if (command === "undo" && restoreEditorHistory("undo"))
|
|
154
|
+
return;
|
|
155
|
+
if (command === "redo" && restoreEditorHistory("redo"))
|
|
156
|
+
return;
|
|
142
157
|
if (!restoreSavedSelection()) {
|
|
143
158
|
safeSelectRange(getSelectionRangeInEditor());
|
|
144
159
|
}
|
|
@@ -164,7 +179,22 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
164
179
|
catch { }
|
|
165
180
|
};
|
|
166
181
|
const applyFormatBlock = (blockName) => {
|
|
167
|
-
|
|
182
|
+
try {
|
|
183
|
+
if (!restoreSavedSelection()) {
|
|
184
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
185
|
+
}
|
|
186
|
+
if (applyFormatBlockFallback(blockName)) {
|
|
187
|
+
const tag = normalizeBlockTag(blockName);
|
|
188
|
+
if (tag === "p" || tag === "h1" || tag === "h2" || tag === "h3") {
|
|
189
|
+
setCurrentBlockType(tag);
|
|
190
|
+
}
|
|
191
|
+
handleInput();
|
|
192
|
+
requestAnimationFrame(updateActiveState);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
exec("formatBlock", blockName);
|
|
196
|
+
}
|
|
197
|
+
catch { }
|
|
168
198
|
};
|
|
169
199
|
const emitChange = () => {
|
|
170
200
|
const el = editableRef.current;
|
|
@@ -176,6 +206,63 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
176
206
|
onChange(html);
|
|
177
207
|
}
|
|
178
208
|
};
|
|
209
|
+
const pushEditorHistory = () => {
|
|
210
|
+
const editor = editableRef.current;
|
|
211
|
+
if (!editor)
|
|
212
|
+
return;
|
|
213
|
+
const selectionCells = selectionRef.current
|
|
214
|
+
? getCellsInGridRect(selectionRef.current.tbody, selectionRef.current.sr, selectionRef.current.sc, selectionRef.current.er, selectionRef.current.ec)
|
|
215
|
+
: [];
|
|
216
|
+
const decor = selectionCells.map((cell) => ({
|
|
217
|
+
cell,
|
|
218
|
+
background: cell.style.background,
|
|
219
|
+
outline: cell.style.outline,
|
|
220
|
+
outlineOffset: cell.style.outlineOffset,
|
|
221
|
+
previousBackground: cell.__rtePrevBg,
|
|
222
|
+
}));
|
|
223
|
+
decor.forEach(({ cell, previousBackground }) => {
|
|
224
|
+
if (previousBackground != null)
|
|
225
|
+
cell.style.background = previousBackground || "";
|
|
226
|
+
cell.style.outline = "";
|
|
227
|
+
cell.style.outlineOffset = "";
|
|
228
|
+
});
|
|
229
|
+
const html = editor.innerHTML;
|
|
230
|
+
decor.forEach(({ cell, background, outline, outlineOffset }) => {
|
|
231
|
+
cell.style.background = background;
|
|
232
|
+
cell.style.outline = outline;
|
|
233
|
+
cell.style.outlineOffset = outlineOffset;
|
|
234
|
+
});
|
|
235
|
+
const undo = historyRef.current.undo;
|
|
236
|
+
if (undo[undo.length - 1] !== html) {
|
|
237
|
+
undo.push(html);
|
|
238
|
+
if (undo.length > 100)
|
|
239
|
+
undo.shift();
|
|
240
|
+
}
|
|
241
|
+
historyRef.current.redo = [];
|
|
242
|
+
};
|
|
243
|
+
const restoreEditorHistory = (dir) => {
|
|
244
|
+
const editor = editableRef.current;
|
|
245
|
+
if (!editor)
|
|
246
|
+
return false;
|
|
247
|
+
const history = historyRef.current;
|
|
248
|
+
const from = dir === "undo" ? history.undo : history.redo;
|
|
249
|
+
const to = dir === "undo" ? history.redo : history.undo;
|
|
250
|
+
const html = from.pop();
|
|
251
|
+
if (html == null)
|
|
252
|
+
return false;
|
|
253
|
+
to.push(editor.innerHTML);
|
|
254
|
+
editor.innerHTML = html;
|
|
255
|
+
fixNegativeMargins(editor);
|
|
256
|
+
ensureTableWrappers(editor);
|
|
257
|
+
addTableResizeHandles();
|
|
258
|
+
clearSelectionDecor();
|
|
259
|
+
setTableMenu(null);
|
|
260
|
+
if (html !== lastEmittedRef.current) {
|
|
261
|
+
lastEmittedRef.current = html;
|
|
262
|
+
onChange?.(html);
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
};
|
|
179
266
|
const restoreSavedSelection = () => {
|
|
180
267
|
const editor = editableRef.current;
|
|
181
268
|
if (!editor)
|
|
@@ -321,19 +408,71 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
321
408
|
cell.parentElement?.replaceChild(replacement, cell);
|
|
322
409
|
return replacement;
|
|
323
410
|
};
|
|
324
|
-
const
|
|
325
|
-
const editor = editableRef.current;
|
|
326
|
-
const block = getCurrentBlock();
|
|
411
|
+
const normalizeBlockTag = (blockName) => {
|
|
327
412
|
const tag = blockName.replace(/[<>]/g, "").toLowerCase() || "p";
|
|
328
|
-
|
|
329
|
-
|
|
413
|
+
return /^(p|h1|h2|h3|h4|h5|h6|pre|blockquote)$/.test(tag) ? tag : null;
|
|
414
|
+
};
|
|
415
|
+
const sortInDocumentOrder = (elements) => {
|
|
416
|
+
return [...elements].sort((a, b) => {
|
|
417
|
+
if (a === b)
|
|
418
|
+
return 0;
|
|
419
|
+
const position = a.compareDocumentPosition(b);
|
|
420
|
+
return position & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
|
421
|
+
});
|
|
422
|
+
};
|
|
423
|
+
const replaceBlockTag = (block, tag) => {
|
|
424
|
+
if (block.tagName.toLowerCase() === tag)
|
|
425
|
+
return block;
|
|
330
426
|
const replacement = document.createElement(tag);
|
|
331
427
|
copyCellOrBlockStyles(block, replacement);
|
|
332
428
|
block.parentElement?.replaceChild(replacement, block);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
429
|
+
return replacement;
|
|
430
|
+
};
|
|
431
|
+
const applyFormatBlockFallback = (blockName) => {
|
|
432
|
+
const editor = editableRef.current;
|
|
433
|
+
const range = getSelectionRangeInEditor();
|
|
434
|
+
const tag = normalizeBlockTag(blockName);
|
|
435
|
+
if (!editor || !range || !tag)
|
|
436
|
+
return false;
|
|
437
|
+
if (!editor.innerHTML.trim()) {
|
|
438
|
+
const block = document.createElement(tag);
|
|
439
|
+
block.innerHTML = "<br>";
|
|
440
|
+
editor.appendChild(block);
|
|
441
|
+
focusElementEnd(block);
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
if (range.collapsed) {
|
|
445
|
+
const block = getCurrentBlock();
|
|
446
|
+
if (!block || block === editor || block.closest("ul,ol") || !block.parentElement)
|
|
447
|
+
return false;
|
|
448
|
+
const replacement = replaceBlockTag(block, tag);
|
|
449
|
+
focusElementEnd(replacement);
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
const selectedBlocks = sortInDocumentOrder(getSelectedBlocks(range)).filter((block) => {
|
|
453
|
+
if (!editor.contains(block) || block === editor)
|
|
454
|
+
return false;
|
|
455
|
+
if (block.closest("ul,ol"))
|
|
456
|
+
return false;
|
|
457
|
+
return Boolean(block.parentElement);
|
|
458
|
+
});
|
|
459
|
+
if (selectedBlocks.length > 0) {
|
|
460
|
+
let lastReplacement = null;
|
|
461
|
+
selectedBlocks.forEach((block) => {
|
|
462
|
+
lastReplacement = replaceBlockTag(block, tag);
|
|
463
|
+
});
|
|
464
|
+
if (lastReplacement)
|
|
465
|
+
focusElementEnd(lastReplacement);
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
const block = document.createElement(tag);
|
|
469
|
+
const contents = range.extractContents();
|
|
470
|
+
block.appendChild(contents);
|
|
471
|
+
if (!block.innerHTML.trim())
|
|
472
|
+
block.innerHTML = "<br>";
|
|
473
|
+
range.insertNode(block);
|
|
474
|
+
focusElementEnd(block);
|
|
475
|
+
return true;
|
|
337
476
|
};
|
|
338
477
|
const cloneListShell = (list, tagName) => {
|
|
339
478
|
const clone = document.createElement(tagName || list.tagName.toLowerCase());
|
|
@@ -544,7 +683,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
544
683
|
};
|
|
545
684
|
const toggleBlockquote = () => {
|
|
546
685
|
try {
|
|
686
|
+
if (!restoreSavedSelection())
|
|
687
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
547
688
|
const range = getSelectionRangeInEditor();
|
|
689
|
+
const editor = editableRef.current;
|
|
690
|
+
if (!editor || !range)
|
|
691
|
+
return;
|
|
692
|
+
const unwrapQuote = (quote) => {
|
|
693
|
+
const parent = quote.parentElement;
|
|
694
|
+
if (!parent)
|
|
695
|
+
return false;
|
|
696
|
+
const moved = [];
|
|
697
|
+
while (quote.firstChild) {
|
|
698
|
+
const child = quote.firstChild;
|
|
699
|
+
parent.insertBefore(child, quote);
|
|
700
|
+
if (child instanceof HTMLElement)
|
|
701
|
+
moved.push(child);
|
|
702
|
+
}
|
|
703
|
+
quote.remove();
|
|
704
|
+
focusElementEnd(moved[moved.length - 1] || parent);
|
|
705
|
+
return true;
|
|
706
|
+
};
|
|
707
|
+
const wrapBlocks = (blocks) => {
|
|
708
|
+
const selected = sortInDocumentOrder(blocks).filter((block) => {
|
|
709
|
+
if (!editor.contains(block) || block === editor)
|
|
710
|
+
return false;
|
|
711
|
+
if (block.closest("ul,ol"))
|
|
712
|
+
return false;
|
|
713
|
+
if (block.tagName.toLowerCase() === "blockquote")
|
|
714
|
+
return false;
|
|
715
|
+
return Boolean(block.parentElement);
|
|
716
|
+
});
|
|
717
|
+
if (selected.length === 0)
|
|
718
|
+
return false;
|
|
719
|
+
let lastWrapped = null;
|
|
720
|
+
selected.forEach((block) => {
|
|
721
|
+
const quote = document.createElement("blockquote");
|
|
722
|
+
block.parentElement?.insertBefore(quote, block);
|
|
723
|
+
quote.appendChild(block);
|
|
724
|
+
lastWrapped = block;
|
|
725
|
+
});
|
|
726
|
+
if (lastWrapped)
|
|
727
|
+
focusElementEnd(lastWrapped);
|
|
728
|
+
return true;
|
|
729
|
+
};
|
|
548
730
|
if (!range)
|
|
549
731
|
return;
|
|
550
732
|
let node = range.commonAncestorContainer;
|
|
@@ -553,16 +735,36 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
553
735
|
const element = node;
|
|
554
736
|
const quote = element?.closest?.('blockquote');
|
|
555
737
|
if (quote && editableRef.current?.contains(quote)) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
safeSelectRange(nextRange);
|
|
563
|
-
handleInput();
|
|
564
|
-
return;
|
|
738
|
+
pushEditorHistory();
|
|
739
|
+
if (unwrapQuote(quote)) {
|
|
740
|
+
handleInput();
|
|
741
|
+
requestAnimationFrame(updateActiveState);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
565
744
|
}
|
|
745
|
+
if (range.collapsed) {
|
|
746
|
+
const block = getCurrentBlock();
|
|
747
|
+
if (block) {
|
|
748
|
+
pushEditorHistory();
|
|
749
|
+
if (!wrapBlocks([block]))
|
|
750
|
+
return;
|
|
751
|
+
handleInput();
|
|
752
|
+
requestAnimationFrame(updateActiveState);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
const blocks = getSelectedBlocks(range);
|
|
758
|
+
if (blocks.length > 0) {
|
|
759
|
+
pushEditorHistory();
|
|
760
|
+
if (!wrapBlocks(blocks))
|
|
761
|
+
return;
|
|
762
|
+
handleInput();
|
|
763
|
+
requestAnimationFrame(updateActiveState);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
pushEditorHistory();
|
|
566
768
|
exec("formatBlock", "<blockquote>");
|
|
567
769
|
}
|
|
568
770
|
catch { }
|
|
@@ -685,8 +887,158 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
685
887
|
const applyTextColor = (color) => {
|
|
686
888
|
exec("foreColor", color);
|
|
687
889
|
};
|
|
890
|
+
const parseCssColor = (value) => {
|
|
891
|
+
const normalized = value.trim().toLowerCase();
|
|
892
|
+
if (!normalized || normalized === "transparent")
|
|
893
|
+
return null;
|
|
894
|
+
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(normalized);
|
|
895
|
+
if (hex) {
|
|
896
|
+
const raw = hex[1].length === 3
|
|
897
|
+
? hex[1].split("").map((part) => part + part).join("")
|
|
898
|
+
: hex[1];
|
|
899
|
+
return {
|
|
900
|
+
r: parseInt(raw.slice(0, 2), 16),
|
|
901
|
+
g: parseInt(raw.slice(2, 4), 16),
|
|
902
|
+
b: parseInt(raw.slice(4, 6), 16),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
const rgb = /^rgba?\(([^)]+)\)$/.exec(normalized);
|
|
906
|
+
if (!rgb)
|
|
907
|
+
return null;
|
|
908
|
+
const parts = rgb[1].split(",").map((part) => Number(part.trim()));
|
|
909
|
+
if (parts.length < 3 || parts.some((part) => !Number.isFinite(part)))
|
|
910
|
+
return null;
|
|
911
|
+
if (parts.length >= 4 && parts[3] === 0)
|
|
912
|
+
return null;
|
|
913
|
+
return {
|
|
914
|
+
r: Math.max(0, Math.min(255, parts[0])),
|
|
915
|
+
g: Math.max(0, Math.min(255, parts[1])),
|
|
916
|
+
b: Math.max(0, Math.min(255, parts[2])),
|
|
917
|
+
};
|
|
918
|
+
};
|
|
919
|
+
const cssColorToHex = (value) => {
|
|
920
|
+
const color = parseCssColor(value);
|
|
921
|
+
if (!color)
|
|
922
|
+
return "";
|
|
923
|
+
const toHex = (part) => Math.round(part).toString(16).padStart(2, "0");
|
|
924
|
+
return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`;
|
|
925
|
+
};
|
|
926
|
+
const getRangeStartElement = (range) => {
|
|
927
|
+
const editor = editableRef.current;
|
|
928
|
+
if (!editor || !range)
|
|
929
|
+
return null;
|
|
930
|
+
let node = range.startContainer;
|
|
931
|
+
if (node === editor && editor.childNodes[range.startOffset]) {
|
|
932
|
+
node = editor.childNodes[range.startOffset];
|
|
933
|
+
}
|
|
934
|
+
if (node.nodeType === Node.TEXT_NODE)
|
|
935
|
+
node = node.parentElement;
|
|
936
|
+
return node instanceof HTMLElement && editor.contains(node) ? node : null;
|
|
937
|
+
};
|
|
938
|
+
const getInlineBackgroundHex = (element) => {
|
|
939
|
+
const editor = editableRef.current;
|
|
940
|
+
let current = element;
|
|
941
|
+
while (current && current !== editor) {
|
|
942
|
+
const inlineHex = cssColorToHex(current.style.backgroundColor || current.style.background);
|
|
943
|
+
if (inlineHex)
|
|
944
|
+
return inlineHex;
|
|
945
|
+
current = current.parentElement;
|
|
946
|
+
}
|
|
947
|
+
return "";
|
|
948
|
+
};
|
|
949
|
+
const currentPickerColorHex = () => {
|
|
950
|
+
if (typeof window === "undefined")
|
|
951
|
+
return colorPickerType === "text" ? "#000000" : "#ffffff";
|
|
952
|
+
const editor = editableRef.current;
|
|
953
|
+
if (!editor)
|
|
954
|
+
return colorPickerType === "text" ? "#000000" : "#ffffff";
|
|
955
|
+
const element = getRangeStartElement(getSelectionRangeInEditor());
|
|
956
|
+
if (colorPickerType === "background") {
|
|
957
|
+
return getInlineBackgroundHex(element) || "#ffffff";
|
|
958
|
+
}
|
|
959
|
+
const target = element || editor;
|
|
960
|
+
return cssColorToHex(window.getComputedStyle(target).color) || "#000000";
|
|
961
|
+
};
|
|
962
|
+
const relativeLuminance = (color) => {
|
|
963
|
+
const channel = (value) => {
|
|
964
|
+
const next = value / 255;
|
|
965
|
+
return next <= 0.03928 ? next / 12.92 : Math.pow((next + 0.055) / 1.055, 2.4);
|
|
966
|
+
};
|
|
967
|
+
return 0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b);
|
|
968
|
+
};
|
|
969
|
+
const contrastRatio = (fg, bg) => {
|
|
970
|
+
const lighter = Math.max(relativeLuminance(fg), relativeLuminance(bg));
|
|
971
|
+
const darker = Math.min(relativeLuminance(fg), relativeLuminance(bg));
|
|
972
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
973
|
+
};
|
|
974
|
+
const readableTextColorForBackground = (background) => {
|
|
975
|
+
const bg = parseCssColor(background);
|
|
976
|
+
if (!bg)
|
|
977
|
+
return "";
|
|
978
|
+
const candidates = ["#111827", "#f9fafb", "#1f2937", "#ffffff", "#000000", "#374151", "#e5e7eb"];
|
|
979
|
+
return candidates
|
|
980
|
+
.map((candidate) => ({
|
|
981
|
+
color: candidate,
|
|
982
|
+
ratio: contrastRatio(parseCssColor(candidate), bg),
|
|
983
|
+
}))
|
|
984
|
+
.sort((a, b) => b.ratio - a.ratio)[0]?.color || "";
|
|
985
|
+
};
|
|
688
986
|
const applyBackgroundColor = (color) => {
|
|
689
|
-
|
|
987
|
+
try {
|
|
988
|
+
const editor = editableRef.current;
|
|
989
|
+
if (!editor)
|
|
990
|
+
return;
|
|
991
|
+
if (!restoreSavedSelection())
|
|
992
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
993
|
+
const range = getSelectionRangeInEditor();
|
|
994
|
+
const readableColor = readableTextColorForBackground(color);
|
|
995
|
+
if (!range)
|
|
996
|
+
return;
|
|
997
|
+
const applyToElement = (element) => {
|
|
998
|
+
element.style.backgroundColor = color;
|
|
999
|
+
if (readableColor)
|
|
1000
|
+
element.style.color = readableColor;
|
|
1001
|
+
};
|
|
1002
|
+
if (range.collapsed) {
|
|
1003
|
+
const span = document.createElement("span");
|
|
1004
|
+
applyToElement(span);
|
|
1005
|
+
span.textContent = "\u200B";
|
|
1006
|
+
range.insertNode(span);
|
|
1007
|
+
const nextRange = document.createRange();
|
|
1008
|
+
nextRange.setStart(span.firstChild || span, 1);
|
|
1009
|
+
nextRange.collapse(true);
|
|
1010
|
+
safeSelectRange(nextRange);
|
|
1011
|
+
savedRangeRef.current = nextRange.cloneRange();
|
|
1012
|
+
handleInput();
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const selectedCells = Array.from(editor.querySelectorAll("td,th"))
|
|
1016
|
+
.filter((cell) => {
|
|
1017
|
+
try {
|
|
1018
|
+
return range.intersectsNode(cell);
|
|
1019
|
+
}
|
|
1020
|
+
catch {
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
if (selectedCells.length > 0) {
|
|
1025
|
+
selectedCells.forEach(applyToElement);
|
|
1026
|
+
handleInput();
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const span = document.createElement("span");
|
|
1030
|
+
applyToElement(span);
|
|
1031
|
+
const fragment = range.extractContents();
|
|
1032
|
+
span.appendChild(fragment);
|
|
1033
|
+
range.insertNode(span);
|
|
1034
|
+
range.selectNodeContents(span);
|
|
1035
|
+
safeSelectRange(range);
|
|
1036
|
+
savedRangeRef.current = range.cloneRange();
|
|
1037
|
+
handleInput();
|
|
1038
|
+
}
|
|
1039
|
+
catch {
|
|
1040
|
+
exec("hiliteColor", color);
|
|
1041
|
+
}
|
|
690
1042
|
};
|
|
691
1043
|
const insertImage = () => {
|
|
692
1044
|
if (!media)
|
|
@@ -1788,9 +2140,123 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1788
2140
|
link.remove();
|
|
1789
2141
|
URL.revokeObjectURL(url);
|
|
1790
2142
|
};
|
|
2143
|
+
const buildStandaloneHtmlDocument = (html) => `<!doctype html>
|
|
2144
|
+
<html lang="en">
|
|
2145
|
+
<head>
|
|
2146
|
+
<meta charset="utf-8">
|
|
2147
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2148
|
+
<title>Smart RTE Export</title>
|
|
2149
|
+
<style>
|
|
2150
|
+
:root {
|
|
2151
|
+
--srte-bg: #ffffff;
|
|
2152
|
+
--srte-text: #111111;
|
|
2153
|
+
--srte-text-muted: #4b5563;
|
|
2154
|
+
--srte-border: #d1d5db;
|
|
2155
|
+
--srte-border-light: #e5e7eb;
|
|
2156
|
+
--srte-accent: #1e90ff;
|
|
2157
|
+
--srte-accent-bg: rgba(30, 144, 255, 0.15);
|
|
2158
|
+
--srte-surface-subtle: #f3f4f6;
|
|
2159
|
+
}
|
|
2160
|
+
html, body {
|
|
2161
|
+
margin: 0;
|
|
2162
|
+
background: var(--srte-bg);
|
|
2163
|
+
color: var(--srte-text);
|
|
2164
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2165
|
+
line-height: 1.6;
|
|
2166
|
+
}
|
|
2167
|
+
body {
|
|
2168
|
+
padding: 32px;
|
|
2169
|
+
}
|
|
2170
|
+
.srte-export {
|
|
2171
|
+
max-width: 960px;
|
|
2172
|
+
margin: 0 auto;
|
|
2173
|
+
}
|
|
2174
|
+
.srte-export h1,
|
|
2175
|
+
.srte-export h2,
|
|
2176
|
+
.srte-export h3,
|
|
2177
|
+
.srte-export h4,
|
|
2178
|
+
.srte-export h5,
|
|
2179
|
+
.srte-export h6 {
|
|
2180
|
+
line-height: 1.3;
|
|
2181
|
+
margin: 1.25em 0 0.5em;
|
|
2182
|
+
font-weight: 700;
|
|
2183
|
+
color: var(--srte-text);
|
|
2184
|
+
}
|
|
2185
|
+
.srte-export p {
|
|
2186
|
+
margin: 0 0 0.85em;
|
|
2187
|
+
}
|
|
2188
|
+
.srte-export blockquote {
|
|
2189
|
+
border-left: 4px solid var(--srte-accent);
|
|
2190
|
+
margin: 0.75em 0;
|
|
2191
|
+
padding: 0.5em 1em;
|
|
2192
|
+
background: var(--srte-surface-subtle);
|
|
2193
|
+
color: var(--srte-text);
|
|
2194
|
+
}
|
|
2195
|
+
.srte-export ul,
|
|
2196
|
+
.srte-export ol {
|
|
2197
|
+
margin: 0.75em 0;
|
|
2198
|
+
padding-left: 1.75em;
|
|
2199
|
+
list-style-position: outside;
|
|
2200
|
+
}
|
|
2201
|
+
.srte-export ul {
|
|
2202
|
+
list-style-type: disc;
|
|
2203
|
+
}
|
|
2204
|
+
.srte-export ol {
|
|
2205
|
+
list-style-type: decimal;
|
|
2206
|
+
}
|
|
2207
|
+
.srte-export li {
|
|
2208
|
+
display: list-item;
|
|
2209
|
+
margin: 0.25em 0;
|
|
2210
|
+
padding-left: 0.25em;
|
|
2211
|
+
}
|
|
2212
|
+
.srte-export table {
|
|
2213
|
+
border-collapse: collapse;
|
|
2214
|
+
width: 100%;
|
|
2215
|
+
margin: 12px 0;
|
|
2216
|
+
}
|
|
2217
|
+
.srte-export td,
|
|
2218
|
+
.srte-export th {
|
|
2219
|
+
border: 1px solid var(--srte-border);
|
|
2220
|
+
padding: 8px;
|
|
2221
|
+
vertical-align: top;
|
|
2222
|
+
text-align: left;
|
|
2223
|
+
}
|
|
2224
|
+
.srte-export th {
|
|
2225
|
+
background: var(--srte-surface-subtle);
|
|
2226
|
+
font-weight: 700;
|
|
2227
|
+
}
|
|
2228
|
+
.srte-export img {
|
|
2229
|
+
max-width: 100%;
|
|
2230
|
+
height: auto;
|
|
2231
|
+
}
|
|
2232
|
+
.srte-export pre,
|
|
2233
|
+
.srte-export code {
|
|
2234
|
+
background: var(--srte-surface-subtle);
|
|
2235
|
+
color: var(--srte-text);
|
|
2236
|
+
border-radius: 4px;
|
|
2237
|
+
}
|
|
2238
|
+
.srte-export pre {
|
|
2239
|
+
padding: 12px;
|
|
2240
|
+
overflow-x: auto;
|
|
2241
|
+
white-space: pre-wrap;
|
|
2242
|
+
}
|
|
2243
|
+
@media print {
|
|
2244
|
+
body {
|
|
2245
|
+
padding: 0;
|
|
2246
|
+
}
|
|
2247
|
+
.srte-export {
|
|
2248
|
+
max-width: none;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
</style>
|
|
2252
|
+
</head>
|
|
2253
|
+
<body>
|
|
2254
|
+
<main class="srte-export">${html || "<p></p>"}</main>
|
|
2255
|
+
</body>
|
|
2256
|
+
</html>`;
|
|
1791
2257
|
const exportHtml = () => {
|
|
1792
2258
|
const html = editableRef.current?.innerHTML || "";
|
|
1793
|
-
downloadText("smart-rte-export.html", html, "text/html");
|
|
2259
|
+
downloadText("smart-rte-export.html", buildStandaloneHtmlDocument(html), "text/html;charset=utf-8");
|
|
1794
2260
|
};
|
|
1795
2261
|
const exportMarkdown = () => {
|
|
1796
2262
|
const html = editableRef.current?.innerHTML || "";
|
|
@@ -2121,6 +2587,35 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2121
2587
|
console.error("Error wrapping tables", e);
|
|
2122
2588
|
}
|
|
2123
2589
|
};
|
|
2590
|
+
const isCaretBoundaryBlock = (node) => {
|
|
2591
|
+
if (!(node instanceof HTMLElement))
|
|
2592
|
+
return false;
|
|
2593
|
+
const tag = node.tagName.toLowerCase();
|
|
2594
|
+
return (tag === "blockquote" ||
|
|
2595
|
+
tag === "table" ||
|
|
2596
|
+
node.getAttribute("data-table-wrapper") === "true");
|
|
2597
|
+
};
|
|
2598
|
+
const isEmptyCaretParagraph = (node) => {
|
|
2599
|
+
if (!(node instanceof HTMLParagraphElement))
|
|
2600
|
+
return false;
|
|
2601
|
+
return !node.textContent?.trim();
|
|
2602
|
+
};
|
|
2603
|
+
const createCaretParagraph = () => {
|
|
2604
|
+
const paragraph = document.createElement("p");
|
|
2605
|
+
paragraph.innerHTML = "<br>";
|
|
2606
|
+
paragraph.setAttribute("data-srte-caret-boundary", "true");
|
|
2607
|
+
return paragraph;
|
|
2608
|
+
};
|
|
2609
|
+
const ensureCaretBoundaryParagraphs = (root) => {
|
|
2610
|
+
const first = root.firstElementChild;
|
|
2611
|
+
if (isCaretBoundaryBlock(first) && !isEmptyCaretParagraph(first.previousSibling)) {
|
|
2612
|
+
root.insertBefore(createCaretParagraph(), first);
|
|
2613
|
+
}
|
|
2614
|
+
const last = root.lastElementChild;
|
|
2615
|
+
if (isCaretBoundaryBlock(last) && !isEmptyCaretParagraph(last.nextSibling)) {
|
|
2616
|
+
root.appendChild(createCaretParagraph());
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2124
2619
|
const handleInput = () => {
|
|
2125
2620
|
if (isComposingRef.current)
|
|
2126
2621
|
return;
|
|
@@ -2131,6 +2626,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2131
2626
|
fixNegativeMargins(el);
|
|
2132
2627
|
// Ensure tables are wrapped for horizontal scrolling
|
|
2133
2628
|
ensureTableWrappers(el);
|
|
2629
|
+
// Keep a reachable typing position around isolating blocks at document edges
|
|
2630
|
+
ensureCaretBoundaryParagraphs(el);
|
|
2134
2631
|
// Add resize handles to tables
|
|
2135
2632
|
addTableResizeHandles();
|
|
2136
2633
|
if (!onChange)
|
|
@@ -2161,8 +2658,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2161
2658
|
const el = editableRef.current;
|
|
2162
2659
|
if (!el)
|
|
2163
2660
|
return;
|
|
2164
|
-
|
|
2165
|
-
|
|
2661
|
+
if (!restoreSavedSelection()) {
|
|
2662
|
+
el.focus();
|
|
2663
|
+
}
|
|
2166
2664
|
let sel = window.getSelection();
|
|
2167
2665
|
let range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
|
|
2168
2666
|
if (!range || !el.contains(range.commonAncestorContainer)) {
|
|
@@ -2181,10 +2679,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2181
2679
|
const node = wrapper.firstChild;
|
|
2182
2680
|
if (!node || !range)
|
|
2183
2681
|
return;
|
|
2682
|
+
pushEditorHistory();
|
|
2683
|
+
if (!range.collapsed)
|
|
2684
|
+
range.deleteContents();
|
|
2184
2685
|
range.insertNode(node);
|
|
2185
2686
|
// Add resize handles to the new table
|
|
2186
|
-
|
|
2187
|
-
|
|
2687
|
+
const insertedTable = node instanceof HTMLTableElement ? node : node.querySelector("table");
|
|
2688
|
+
if (insertedTable) {
|
|
2689
|
+
const tbody = insertedTable.querySelector('tbody');
|
|
2188
2690
|
if (tbody) {
|
|
2189
2691
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
2190
2692
|
rows.forEach((row, index) => {
|
|
@@ -2339,6 +2841,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2339
2841
|
const anchor = grid[sr]?.[sc];
|
|
2340
2842
|
if (!anchor)
|
|
2341
2843
|
return;
|
|
2844
|
+
pushEditorHistory();
|
|
2342
2845
|
// Collect content and remove other cells
|
|
2343
2846
|
const contents = [];
|
|
2344
2847
|
const cellsToMerge = getCellsInGridRect(tbody, sr, sc, er, ec);
|
|
@@ -2526,19 +3029,43 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2526
3029
|
handleInput();
|
|
2527
3030
|
};
|
|
2528
3031
|
const applyBgToSelection = (hex, fallbackCell) => {
|
|
3032
|
+
const readableColor = readableTextColorForBackground(hex);
|
|
3033
|
+
const applyFill = (cell) => {
|
|
3034
|
+
cell.style.background = hex;
|
|
3035
|
+
if (readableColor)
|
|
3036
|
+
cell.style.color = readableColor;
|
|
3037
|
+
};
|
|
2529
3038
|
const sel = shouldUseTableSelection(fallbackCell) ? selectionRef.current : null;
|
|
2530
3039
|
if (sel) {
|
|
2531
3040
|
const cells = getCellsInGridRect(sel.tbody, sel.sr, sel.sc, sel.er, sel.ec);
|
|
2532
3041
|
clearSelectionDecor();
|
|
2533
|
-
cells.forEach(
|
|
2534
|
-
cell.style.background = hex;
|
|
2535
|
-
});
|
|
3042
|
+
cells.forEach(applyFill);
|
|
2536
3043
|
}
|
|
2537
3044
|
else if (fallbackCell) {
|
|
2538
3045
|
clearSelectionDecor();
|
|
2539
|
-
fallbackCell
|
|
3046
|
+
applyFill(fallbackCell);
|
|
2540
3047
|
}
|
|
2541
3048
|
};
|
|
3049
|
+
const tableCellFillHex = (cell) => {
|
|
3050
|
+
const storedSelectionBackground = cell.__rtePrevBg;
|
|
3051
|
+
const inlineBackground = typeof storedSelectionBackground === "string"
|
|
3052
|
+
? storedSelectionBackground
|
|
3053
|
+
: cell.style.backgroundColor || cell.style.background;
|
|
3054
|
+
const inlineHex = cssColorToHex(inlineBackground || "");
|
|
3055
|
+
if (inlineHex)
|
|
3056
|
+
return inlineHex;
|
|
3057
|
+
const computedHex = cssColorToHex(window.getComputedStyle(cell).backgroundColor);
|
|
3058
|
+
return computedHex || "#ffffff";
|
|
3059
|
+
};
|
|
3060
|
+
const tableMenuFillHex = (cell) => {
|
|
3061
|
+
const sel = shouldUseTableSelection(cell) ? selectionRef.current : null;
|
|
3062
|
+
if (!sel)
|
|
3063
|
+
return tableCellFillHex(cell);
|
|
3064
|
+
const cells = getCellsInGridRect(sel.tbody, sel.sr, sel.sc, sel.er, sel.ec);
|
|
3065
|
+
const first = cells[0] ? tableCellFillHex(cells[0]) : tableCellFillHex(cell);
|
|
3066
|
+
const allSame = cells.every((candidate) => tableCellFillHex(candidate) === first);
|
|
3067
|
+
return allSame ? first : tableCellFillHex(cell);
|
|
3068
|
+
};
|
|
2542
3069
|
const toggleBorderSelection = (fallbackCell) => {
|
|
2543
3070
|
const applyToggle = (cell) => {
|
|
2544
3071
|
const cur = cell.style.border;
|
|
@@ -2554,6 +3081,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2554
3081
|
}
|
|
2555
3082
|
};
|
|
2556
3083
|
const runTableCellAction = (cell, action) => {
|
|
3084
|
+
pushEditorHistory();
|
|
2557
3085
|
action(cell);
|
|
2558
3086
|
handleInput();
|
|
2559
3087
|
setTableMenu(null);
|
|
@@ -2577,6 +3105,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2577
3105
|
const cells = getColumnCells(table, colIndex);
|
|
2578
3106
|
if (cells.length === 0)
|
|
2579
3107
|
return;
|
|
3108
|
+
pushEditorHistory();
|
|
2580
3109
|
const firstCell = cells[0];
|
|
2581
3110
|
const currentWidth = firstCell.offsetWidth;
|
|
2582
3111
|
// Unlock table width so it can grow
|
|
@@ -2603,6 +3132,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2603
3132
|
return;
|
|
2604
3133
|
const cells = cellsOfRow(row);
|
|
2605
3134
|
const currentHeight = row.offsetHeight;
|
|
3135
|
+
pushEditorHistory();
|
|
2606
3136
|
tableResizeRef.current = {
|
|
2607
3137
|
type: 'row',
|
|
2608
3138
|
table,
|
|
@@ -2654,6 +3184,184 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2654
3184
|
handleInput();
|
|
2655
3185
|
}
|
|
2656
3186
|
};
|
|
3187
|
+
const getRangeFromPoint = (x, y) => {
|
|
3188
|
+
let range = null;
|
|
3189
|
+
// @ts-ignore
|
|
3190
|
+
if (document.caretRangeFromPoint) {
|
|
3191
|
+
// @ts-ignore
|
|
3192
|
+
range = document.caretRangeFromPoint(x, y);
|
|
3193
|
+
}
|
|
3194
|
+
else if (document.caretPositionFromPoint) {
|
|
3195
|
+
const pos = document.caretPositionFromPoint(x, y);
|
|
3196
|
+
if (pos) {
|
|
3197
|
+
range = document.createRange();
|
|
3198
|
+
range.setStart(pos.offsetNode, pos.offset);
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
return range;
|
|
3202
|
+
};
|
|
3203
|
+
const getMovableElementFromNode = (node) => {
|
|
3204
|
+
const editor = editableRef.current;
|
|
3205
|
+
if (!editor || !node)
|
|
3206
|
+
return null;
|
|
3207
|
+
let element = node instanceof HTMLElement ? node : node.parentElement;
|
|
3208
|
+
if (!element || !editor.contains(element))
|
|
3209
|
+
return null;
|
|
3210
|
+
const image = element.closest("img");
|
|
3211
|
+
if (image && editor.contains(image)) {
|
|
3212
|
+
return (image.parentElement?.tagName === "A" ? image.parentElement : image);
|
|
3213
|
+
}
|
|
3214
|
+
const tableElement = element.closest("table");
|
|
3215
|
+
if (tableElement && editor.contains(tableElement)) {
|
|
3216
|
+
return (tableElement.closest('[data-table-wrapper="true"]') || tableElement);
|
|
3217
|
+
}
|
|
3218
|
+
const listElement = element.closest("ul,ol");
|
|
3219
|
+
if (listElement && editor.contains(listElement))
|
|
3220
|
+
return listElement;
|
|
3221
|
+
const block = element.closest('[data-table-wrapper="true"],blockquote,pre,p,h1,h2,h3,h4,h5,h6,div');
|
|
3222
|
+
if (!block || block === editor || !editor.contains(block))
|
|
3223
|
+
return null;
|
|
3224
|
+
if (block.getAttribute("data-srte-caret-boundary") === "true")
|
|
3225
|
+
return null;
|
|
3226
|
+
return block;
|
|
3227
|
+
};
|
|
3228
|
+
const updateDragHandleForTarget = (target) => {
|
|
3229
|
+
const scroller = editorScrollRef.current;
|
|
3230
|
+
if (dragHandleHideTimerRef.current != null) {
|
|
3231
|
+
window.clearTimeout(dragHandleHideTimerRef.current);
|
|
3232
|
+
dragHandleHideTimerRef.current = null;
|
|
3233
|
+
}
|
|
3234
|
+
if (!target || !scroller || !editableRef.current?.contains(target)) {
|
|
3235
|
+
setDragHandle(null);
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
const targetRect = target.getBoundingClientRect();
|
|
3239
|
+
const scrollRect = scroller.getBoundingClientRect();
|
|
3240
|
+
const next = {
|
|
3241
|
+
left: Math.max(4, targetRect.left - scrollRect.left + scroller.scrollLeft - 30),
|
|
3242
|
+
top: targetRect.top - scrollRect.top + scroller.scrollTop,
|
|
3243
|
+
height: Math.max(24, targetRect.height),
|
|
3244
|
+
target,
|
|
3245
|
+
};
|
|
3246
|
+
setDragHandle((current) => {
|
|
3247
|
+
if (current?.target === next.target &&
|
|
3248
|
+
Math.abs(current.left - next.left) < 1 &&
|
|
3249
|
+
Math.abs(current.top - next.top) < 1 &&
|
|
3250
|
+
Math.abs(current.height - next.height) < 1) {
|
|
3251
|
+
return current;
|
|
3252
|
+
}
|
|
3253
|
+
return next;
|
|
3254
|
+
});
|
|
3255
|
+
};
|
|
3256
|
+
const scheduleDragHandleHide = () => {
|
|
3257
|
+
if (dragHandleHideTimerRef.current != null) {
|
|
3258
|
+
window.clearTimeout(dragHandleHideTimerRef.current);
|
|
3259
|
+
}
|
|
3260
|
+
dragHandleHideTimerRef.current = window.setTimeout(() => {
|
|
3261
|
+
dragHandleHideTimerRef.current = null;
|
|
3262
|
+
if (!draggedBlockRef.current)
|
|
3263
|
+
setDragHandle(null);
|
|
3264
|
+
}, 120);
|
|
3265
|
+
};
|
|
3266
|
+
const getImageFromMovableBlock = (block) => {
|
|
3267
|
+
if (block.tagName === "IMG")
|
|
3268
|
+
return block;
|
|
3269
|
+
return block.querySelector("img");
|
|
3270
|
+
};
|
|
3271
|
+
const dropBlockAtPoint = (block, x, y) => {
|
|
3272
|
+
const editor = editableRef.current;
|
|
3273
|
+
if (!editor || !editor.contains(block))
|
|
3274
|
+
return false;
|
|
3275
|
+
const under = document.elementFromPoint(x, y);
|
|
3276
|
+
const draggedImage = getImageFromMovableBlock(block);
|
|
3277
|
+
const targetCell = draggedImage ? getClosestCell(under) : null;
|
|
3278
|
+
if (draggedImage && targetCell && !block.contains(targetCell)) {
|
|
3279
|
+
if (targetCell.contains(block))
|
|
3280
|
+
return false;
|
|
3281
|
+
const range = getRangeFromPoint(x, y);
|
|
3282
|
+
if (range && targetCell.contains(range.commonAncestorContainer)) {
|
|
3283
|
+
range.insertNode(block);
|
|
3284
|
+
}
|
|
3285
|
+
else {
|
|
3286
|
+
targetCell.appendChild(block);
|
|
3287
|
+
}
|
|
3288
|
+
return true;
|
|
3289
|
+
}
|
|
3290
|
+
const target = getMovableElementFromNode(under);
|
|
3291
|
+
if (target && target !== block && !block.contains(target) && !target.contains(block)) {
|
|
3292
|
+
const rect = target.getBoundingClientRect();
|
|
3293
|
+
const parent = target.parentElement;
|
|
3294
|
+
if (!parent)
|
|
3295
|
+
return false;
|
|
3296
|
+
if (y < rect.top + rect.height / 2)
|
|
3297
|
+
parent.insertBefore(block, target);
|
|
3298
|
+
else
|
|
3299
|
+
parent.insertBefore(block, target.nextSibling);
|
|
3300
|
+
return true;
|
|
3301
|
+
}
|
|
3302
|
+
const range = getRangeFromPoint(x, y);
|
|
3303
|
+
if (range && editor.contains(range.commonAncestorContainer)) {
|
|
3304
|
+
if (block.contains(range.commonAncestorContainer))
|
|
3305
|
+
return false;
|
|
3306
|
+
range.insertNode(block);
|
|
3307
|
+
return true;
|
|
3308
|
+
}
|
|
3309
|
+
editor.appendChild(block);
|
|
3310
|
+
return true;
|
|
3311
|
+
};
|
|
3312
|
+
const getTopLevelMovableElement = () => {
|
|
3313
|
+
const editor = editableRef.current;
|
|
3314
|
+
if (!editor)
|
|
3315
|
+
return null;
|
|
3316
|
+
const imageTarget = selectedImage?.parentElement?.tagName === "A"
|
|
3317
|
+
? selectedImage.parentElement
|
|
3318
|
+
: selectedImage;
|
|
3319
|
+
if (imageTarget && editor.contains(imageTarget))
|
|
3320
|
+
return imageTarget;
|
|
3321
|
+
const range = getSelectionRangeInEditor();
|
|
3322
|
+
let node = range?.commonAncestorContainer || null;
|
|
3323
|
+
if (node && node.nodeType === Node.TEXT_NODE)
|
|
3324
|
+
node = node.parentNode;
|
|
3325
|
+
let element = node instanceof HTMLElement ? node : null;
|
|
3326
|
+
if (!element)
|
|
3327
|
+
return null;
|
|
3328
|
+
return getMovableElementFromNode(element);
|
|
3329
|
+
};
|
|
3330
|
+
const elementSibling = (element, direction) => {
|
|
3331
|
+
let sibling = direction === "previous" ? element.previousSibling : element.nextSibling;
|
|
3332
|
+
while (sibling && sibling.nodeType === Node.TEXT_NODE && !sibling.textContent?.trim()) {
|
|
3333
|
+
sibling = direction === "previous" ? sibling.previousSibling : sibling.nextSibling;
|
|
3334
|
+
}
|
|
3335
|
+
return sibling;
|
|
3336
|
+
};
|
|
3337
|
+
const moveCurrentElement = (direction) => {
|
|
3338
|
+
const editor = editableRef.current;
|
|
3339
|
+
const target = getTopLevelMovableElement();
|
|
3340
|
+
if (!editor || !target)
|
|
3341
|
+
return;
|
|
3342
|
+
pushEditorHistory();
|
|
3343
|
+
if (direction === "up") {
|
|
3344
|
+
const previous = elementSibling(target, "previous");
|
|
3345
|
+
if (previous)
|
|
3346
|
+
target.parentElement?.insertBefore(target, previous);
|
|
3347
|
+
}
|
|
3348
|
+
else if (direction === "down") {
|
|
3349
|
+
const next = elementSibling(target, "next");
|
|
3350
|
+
if (next)
|
|
3351
|
+
target.parentElement?.insertBefore(next, target);
|
|
3352
|
+
}
|
|
3353
|
+
else {
|
|
3354
|
+
const current = parseInt(target.style.marginLeft || "0", 10) || 0;
|
|
3355
|
+
const nextMargin = direction === "right"
|
|
3356
|
+
? Math.min(current + 24, 240)
|
|
3357
|
+
: Math.max(current - 24, 0);
|
|
3358
|
+
target.style.marginLeft = nextMargin ? `${nextMargin}px` : "";
|
|
3359
|
+
}
|
|
3360
|
+
focusElementEnd(target);
|
|
3361
|
+
setSelectedImage(target.tagName === "IMG" ? target : target.querySelector("img"));
|
|
3362
|
+
scheduleImageOverlay();
|
|
3363
|
+
handleInput();
|
|
3364
|
+
};
|
|
2657
3365
|
const addTableResizeHandles = () => {
|
|
2658
3366
|
if (!table)
|
|
2659
3367
|
return;
|
|
@@ -2760,7 +3468,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2760
3468
|
} }), _jsx("input", { ref: mdInputRef, type: "file", accept: ".md,.markdown,text/markdown,text/plain", style: { display: "none" }, onChange: (e) => {
|
|
2761
3469
|
importTextFile(e.currentTarget.files, "md");
|
|
2762
3470
|
e.currentTarget.value = "";
|
|
2763
|
-
} }), _jsxs("select", {
|
|
3471
|
+
} }), _jsxs("select", { value: currentBlockType, onMouseDown: preserveEditorSelection, onChange: (e) => {
|
|
2764
3472
|
const val = e.target.value;
|
|
2765
3473
|
if (val === "p")
|
|
2766
3474
|
applyFormatBlock("<p>");
|
|
@@ -2777,7 +3485,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2777
3485
|
borderRadius: 6,
|
|
2778
3486
|
background: "var(--srte-input-bg)",
|
|
2779
3487
|
color: "var(--srte-input-text)",
|
|
2780
|
-
}, children: [_jsx("option", { value: "p", children: "Paragraph" }), _jsx("option", { value: "h1", children: "Heading 1" }), _jsx("option", { value: "h2", children: "Heading 2" }), _jsx("option", { value: "h3", children: "Heading 3" })] }), _jsx("button", { title: "Bold", onClick: () => exec("bold"), "aria-pressed": activeState.bold, style: activeButtonStyle(activeState.bold), children: _jsx("span", { style: { fontWeight: 700 }, children: "B" }) }), _jsx("button", { title: "Italic", onClick: () => exec("italic"), "aria-pressed": activeState.italic, style: activeButtonStyle(activeState.italic, { fontStyle: "italic" }), children: "I" }), _jsx("button", { title: "Underline", onClick: () => exec("underline"), "aria-pressed": activeState.underline, style: activeButtonStyle(activeState.underline, { textDecoration: "underline" }), children: "U" }), _jsx("button", { title: "Strikethrough", onClick: () => exec("strikeThrough"), "aria-pressed": activeState.strikeThrough, style: activeButtonStyle(activeState.strikeThrough, { textDecoration: "line-through" }), children: "S" }), _jsxs("select", { value: currentFontSize, onMouseDown: () => {
|
|
3488
|
+
}, children: [_jsx("option", { value: "p", children: "Paragraph" }), _jsx("option", { value: "h1", children: "Heading 1" }), _jsx("option", { value: "h2", children: "Heading 2" }), _jsx("option", { value: "h3", children: "Heading 3" })] }), _jsx("button", { title: "Bold", onClick: () => exec("bold"), "aria-pressed": activeState.bold, style: activeButtonStyle(activeState.bold), children: _jsx("span", { style: { fontWeight: 700 }, children: "B" }) }), _jsx("button", { title: "Italic", onClick: () => exec("italic"), "aria-pressed": activeState.italic, style: activeButtonStyle(activeState.italic, { fontStyle: "italic" }), children: "I" }), _jsx("button", { title: "Underline", onClick: () => exec("underline"), "aria-pressed": activeState.underline, style: activeButtonStyle(activeState.underline, { textDecoration: "underline" }), children: "U" }), _jsx("button", { title: "Strikethrough", onClick: () => exec("strikeThrough"), "aria-pressed": activeState.strikeThrough, style: activeButtonStyle(activeState.strikeThrough, { textDecoration: "line-through" }), children: "S" }), showFontSize && (_jsxs("select", { value: currentFontSize, onMouseDown: () => {
|
|
2781
3489
|
// Save selection before dropdown interaction
|
|
2782
3490
|
const sel = window.getSelection();
|
|
2783
3491
|
if (sel && sel.rangeCount > 0) {
|
|
@@ -2794,7 +3502,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2794
3502
|
borderRadius: 6,
|
|
2795
3503
|
background: "var(--srte-input-bg)",
|
|
2796
3504
|
color: "var(--srte-input-text)",
|
|
2797
|
-
}, 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" })] }), preserveFontFamily && (_jsxs("select", { value: currentFont, onMouseDown: () => {
|
|
3505
|
+
}, 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" })] })), preserveFontFamily && (_jsxs("select", { value: currentFont, onMouseDown: () => {
|
|
2798
3506
|
const sel = window.getSelection();
|
|
2799
3507
|
if (sel && sel.rangeCount > 0) {
|
|
2800
3508
|
const range = sel.getRangeAt(0);
|
|
@@ -3002,7 +3710,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3002
3710
|
borderRadius: 6,
|
|
3003
3711
|
background: "var(--srte-input-bg)",
|
|
3004
3712
|
color: "var(--srte-input-text)",
|
|
3005
|
-
}, children: "\u2795 Table" })),
|
|
3713
|
+
}, children: "\u2795 Table" })), _jsxs("div", { style: {
|
|
3714
|
+
display: "inline-flex",
|
|
3715
|
+
gap: 4,
|
|
3716
|
+
alignItems: "center",
|
|
3717
|
+
marginLeft: 6,
|
|
3718
|
+
}, children: [_jsx("span", { style: { fontSize: 12, opacity: 0.7 }, children: "Move:" }), _jsx("button", { title: "Move selected block up", onClick: () => moveCurrentElement("up"), style: activeButtonStyle(false, { height: 28, minWidth: 28, padding: "0 6px" }), children: "\u2191" }), _jsx("button", { title: "Move selected block down", onClick: () => moveCurrentElement("down"), style: activeButtonStyle(false, { height: 28, minWidth: 28, padding: "0 6px" }), children: "\u2193" }), _jsx("button", { title: "Move selected block left", onClick: () => moveCurrentElement("left"), style: activeButtonStyle(false, { height: 28, minWidth: 28, padding: "0 6px" }), children: "\u2190" }), _jsx("button", { title: "Move selected block right", onClick: () => moveCurrentElement("right"), style: activeButtonStyle(false, { height: 28, minWidth: 28, padding: "0 6px" }), children: "\u2192" })] }), _jsx("button", { title: "Undo", onClick: () => exec("undo"), style: {
|
|
3006
3719
|
height: 32,
|
|
3007
3720
|
padding: "0 10px",
|
|
3008
3721
|
border: "1px solid var(--srte-input-border)",
|
|
@@ -3169,7 +3882,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3169
3882
|
borderRadius: 4,
|
|
3170
3883
|
background: color,
|
|
3171
3884
|
cursor: 'pointer',
|
|
3172
|
-
}, title: color }, color))) }), _jsxs("div", { style: { marginBottom: 12 }, children: [_jsx("label", { style: { display: 'block', marginBottom: 6, fontSize: 12 }, children: "Custom color:" }), _jsx("input", { type: "color", onChange: (e) => {
|
|
3885
|
+
}, title: color }, color))) }), _jsxs("div", { style: { marginBottom: 12 }, children: [_jsx("label", { style: { display: 'block', marginBottom: 6, fontSize: 12 }, children: "Custom color:" }), _jsx("input", { type: "color", value: currentPickerColorHex(), onChange: (e) => {
|
|
3173
3886
|
if (colorPickerType === 'text') {
|
|
3174
3887
|
applyTextColor(e.target.value);
|
|
3175
3888
|
}
|
|
@@ -3338,7 +4051,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3338
4051
|
borderRadius: 4,
|
|
3339
4052
|
background: "var(--srte-input-bg)",
|
|
3340
4053
|
color: "var(--srte-modal-text)",
|
|
3341
|
-
}, title: sym, children: sym }, i)))] })] }) })), _jsxs("div", { ref: editorScrollRef, style: {
|
|
4054
|
+
}, title: sym, children: sym }, i)))] })] }) })), _jsxs("div", { ref: editorScrollRef, onScroll: () => updateDragHandleForTarget(dragHandle?.target || null), style: {
|
|
3342
4055
|
width: "100%",
|
|
3343
4056
|
maxWidth: "100%",
|
|
3344
4057
|
flex: "1 1 auto",
|
|
@@ -3354,6 +4067,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3354
4067
|
}, children: [_jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onInput: handleInput, onKeyUp: updateActiveState, onMouseUp: updateActiveState, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
|
|
3355
4068
|
isComposingRef.current = false;
|
|
3356
4069
|
handleInput();
|
|
4070
|
+
}, onMouseMove: (e) => {
|
|
4071
|
+
if (draggedBlockRef.current)
|
|
4072
|
+
return;
|
|
4073
|
+
updateDragHandleForTarget(getMovableElementFromNode(e.target));
|
|
4074
|
+
}, onMouseLeave: () => {
|
|
4075
|
+
if (!draggedBlockRef.current)
|
|
4076
|
+
scheduleDragHandleHide();
|
|
3357
4077
|
}, onPaste: (e) => {
|
|
3358
4078
|
const items = e.clipboardData?.files;
|
|
3359
4079
|
if (media && items && items.length) {
|
|
@@ -3371,29 +4091,30 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3371
4091
|
}
|
|
3372
4092
|
}, onDragOver: (e) => {
|
|
3373
4093
|
// Allow dragging images within editor and file drops
|
|
3374
|
-
if (
|
|
4094
|
+
if (draggedBlockRef.current ||
|
|
4095
|
+
draggedImageRef.current ||
|
|
3375
4096
|
e.dataTransfer?.types?.includes("Files")) {
|
|
3376
4097
|
e.preventDefault();
|
|
3377
4098
|
}
|
|
3378
4099
|
}, onDrop: (e) => {
|
|
4100
|
+
if (draggedBlockRef.current) {
|
|
4101
|
+
e.preventDefault();
|
|
4102
|
+
const block = draggedBlockRef.current;
|
|
4103
|
+
draggedBlockRef.current = null;
|
|
4104
|
+
pushEditorHistory();
|
|
4105
|
+
if (dropBlockAtPoint(block, e.clientX, e.clientY)) {
|
|
4106
|
+
focusElementEnd(block);
|
|
4107
|
+
updateDragHandleForTarget(block);
|
|
4108
|
+
handleInput();
|
|
4109
|
+
}
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
3379
4112
|
// Move existing dragged image inside editor
|
|
3380
4113
|
if (draggedImageRef.current) {
|
|
3381
4114
|
e.preventDefault();
|
|
3382
4115
|
const x = e.clientX;
|
|
3383
4116
|
const y = e.clientY;
|
|
3384
|
-
let range =
|
|
3385
|
-
// @ts-ignore
|
|
3386
|
-
if (document.caretRangeFromPoint) {
|
|
3387
|
-
// @ts-ignore
|
|
3388
|
-
range = document.caretRangeFromPoint(x, y);
|
|
3389
|
-
}
|
|
3390
|
-
else if (document.caretPositionFromPoint) {
|
|
3391
|
-
const pos = document.caretPositionFromPoint(x, y);
|
|
3392
|
-
if (pos) {
|
|
3393
|
-
range = document.createRange();
|
|
3394
|
-
range.setStart(pos.offsetNode, pos.offset);
|
|
3395
|
-
}
|
|
3396
|
-
}
|
|
4117
|
+
let range = getRangeFromPoint(x, y);
|
|
3397
4118
|
const img = draggedImageRef.current;
|
|
3398
4119
|
draggedImageRef.current = null;
|
|
3399
4120
|
if (range &&
|
|
@@ -3402,6 +4123,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3402
4123
|
// Avoid inserting inside the image itself
|
|
3403
4124
|
if (range.startContainer === img || range.endContainer === img)
|
|
3404
4125
|
return;
|
|
4126
|
+
pushEditorHistory();
|
|
3405
4127
|
// If dropping inside a link, insert right after the link element
|
|
3406
4128
|
let container = range.commonAncestorContainer;
|
|
3407
4129
|
let linkAncestor = null;
|
|
@@ -3434,19 +4156,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3434
4156
|
// Try to move caret to drop point
|
|
3435
4157
|
const x = e.clientX;
|
|
3436
4158
|
const y = e.clientY;
|
|
3437
|
-
let range =
|
|
3438
|
-
// @ts-ignore
|
|
3439
|
-
if (document.caretRangeFromPoint) {
|
|
3440
|
-
// @ts-ignore
|
|
3441
|
-
range = document.caretRangeFromPoint(x, y);
|
|
3442
|
-
}
|
|
3443
|
-
else if (document.caretPositionFromPoint) {
|
|
3444
|
-
const pos = document.caretPositionFromPoint(x, y);
|
|
3445
|
-
if (pos) {
|
|
3446
|
-
range = document.createRange();
|
|
3447
|
-
range.setStart(pos.offsetNode, pos.offset);
|
|
3448
|
-
}
|
|
3449
|
-
}
|
|
4159
|
+
let range = getRangeFromPoint(x, y);
|
|
3450
4160
|
if (range) {
|
|
3451
4161
|
const sel = window.getSelection();
|
|
3452
4162
|
sel?.removeAllRanges();
|
|
@@ -3488,6 +4198,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3488
4198
|
}
|
|
3489
4199
|
}, onDragEnd: () => {
|
|
3490
4200
|
draggedImageRef.current = null;
|
|
4201
|
+
draggedBlockRef.current = null;
|
|
3491
4202
|
}, style: {
|
|
3492
4203
|
minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
|
|
3493
4204
|
maxWidth: "100%",
|
|
@@ -3506,6 +4217,20 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3506
4217
|
}
|
|
3507
4218
|
updateActiveState();
|
|
3508
4219
|
}, onKeyDown: (e) => {
|
|
4220
|
+
if ((e.metaKey || e.ctrlKey) && String(e.key).toLowerCase() === "z") {
|
|
4221
|
+
const restored = restoreEditorHistory(e.shiftKey ? "redo" : "undo");
|
|
4222
|
+
if (restored) {
|
|
4223
|
+
e.preventDefault();
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
if ((e.metaKey || e.ctrlKey) && String(e.key).toLowerCase() === "y") {
|
|
4228
|
+
const restored = restoreEditorHistory("redo");
|
|
4229
|
+
if (restored) {
|
|
4230
|
+
e.preventDefault();
|
|
4231
|
+
return;
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
3509
4234
|
if (formula &&
|
|
3510
4235
|
(e.metaKey || e.ctrlKey) &&
|
|
3511
4236
|
String(e.key).toLowerCase() === "m") {
|
|
@@ -3627,7 +4352,55 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3627
4352
|
setTableMenu(null);
|
|
3628
4353
|
setImageMenu(null);
|
|
3629
4354
|
}
|
|
3630
|
-
} }),
|
|
4355
|
+
} }), dragHandle && !readOnly && (_jsx("button", { type: "button", draggable: true, title: "Drag block", "aria-label": "Drag block", onMouseEnter: () => {
|
|
4356
|
+
if (dragHandleHideTimerRef.current != null) {
|
|
4357
|
+
window.clearTimeout(dragHandleHideTimerRef.current);
|
|
4358
|
+
dragHandleHideTimerRef.current = null;
|
|
4359
|
+
}
|
|
4360
|
+
}, onMouseLeave: () => {
|
|
4361
|
+
if (!draggedBlockRef.current)
|
|
4362
|
+
scheduleDragHandleHide();
|
|
4363
|
+
}, onMouseDown: (e) => {
|
|
4364
|
+
e.stopPropagation();
|
|
4365
|
+
}, onDragStart: (e) => {
|
|
4366
|
+
draggedBlockRef.current = dragHandle.target;
|
|
4367
|
+
e.dataTransfer.effectAllowed = "move";
|
|
4368
|
+
e.dataTransfer.setData("text/plain", "moving-block");
|
|
4369
|
+
try {
|
|
4370
|
+
const ghost = dragHandle.target.cloneNode(true);
|
|
4371
|
+
ghost.style.position = "fixed";
|
|
4372
|
+
ghost.style.left = "-10000px";
|
|
4373
|
+
ghost.style.top = "-10000px";
|
|
4374
|
+
ghost.style.width = `${Math.min(dragHandle.target.getBoundingClientRect().width, 480)}px`;
|
|
4375
|
+
ghost.style.opacity = "0.75";
|
|
4376
|
+
document.body.appendChild(ghost);
|
|
4377
|
+
e.dataTransfer.setDragImage(ghost, 12, 12);
|
|
4378
|
+
window.setTimeout(() => ghost.remove(), 0);
|
|
4379
|
+
}
|
|
4380
|
+
catch { }
|
|
4381
|
+
}, onDragEnd: () => {
|
|
4382
|
+
draggedBlockRef.current = null;
|
|
4383
|
+
updateDragHandleForTarget(dragHandle.target);
|
|
4384
|
+
}, style: {
|
|
4385
|
+
position: "absolute",
|
|
4386
|
+
left: dragHandle.left,
|
|
4387
|
+
top: dragHandle.top + Math.max(0, (dragHandle.height - 24) / 2),
|
|
4388
|
+
width: 24,
|
|
4389
|
+
height: 24,
|
|
4390
|
+
display: "inline-flex",
|
|
4391
|
+
alignItems: "center",
|
|
4392
|
+
justifyContent: "center",
|
|
4393
|
+
border: "1px solid var(--srte-border)",
|
|
4394
|
+
borderRadius: 6,
|
|
4395
|
+
background: "var(--srte-input-bg)",
|
|
4396
|
+
color: "var(--srte-input-text)",
|
|
4397
|
+
boxShadow: "var(--srte-menu-shadow)",
|
|
4398
|
+
cursor: "grab",
|
|
4399
|
+
zIndex: 8,
|
|
4400
|
+
fontSize: 14,
|
|
4401
|
+
lineHeight: 1,
|
|
4402
|
+
padding: 0,
|
|
4403
|
+
}, children: "\u22EE\u22EE" })), selectedImage && imageOverlay && (_jsxs("div", { style: {
|
|
3631
4404
|
position: "absolute",
|
|
3632
4405
|
left: imageOverlay.left,
|
|
3633
4406
|
top: imageOverlay.top,
|
|
@@ -3758,7 +4531,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3758
4531
|
alignItems: "center",
|
|
3759
4532
|
padding: "4px 6px",
|
|
3760
4533
|
fontSize: 12,
|
|
3761
|
-
}, children: [_jsx("span", { children: "Fill:" }), _jsx("input", { type: "color",
|
|
4534
|
+
}, children: [_jsx("span", { children: "Fill:" }), _jsx("input", { type: "color", value: tableMenuFillHex(tableMenu.cell), onChange: (e) => {
|
|
3762
4535
|
runTableCellAction(tableMenu.cell, (cell) => applyBgToSelection(e.target.value, cell));
|
|
3763
4536
|
}, style: {
|
|
3764
4537
|
width: 28,
|
|
@@ -13,10 +13,15 @@ export function MediaManager(props) {
|
|
|
13
13
|
if (!open)
|
|
14
14
|
return;
|
|
15
15
|
setError(null);
|
|
16
|
-
|
|
16
|
+
}, [open]);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!open || activeTab !== "library")
|
|
19
|
+
return;
|
|
20
|
+
const timer = window.setTimeout(() => {
|
|
17
21
|
performSearch();
|
|
18
|
-
}
|
|
19
|
-
|
|
22
|
+
}, 300);
|
|
23
|
+
return () => window.clearTimeout(timer);
|
|
24
|
+
}, [open, activeTab, query]);
|
|
20
25
|
const performSearch = async () => {
|
|
21
26
|
try {
|
|
22
27
|
const items = await adapter.search({ q: query, mimePrefix: "image/" });
|
|
@@ -37,7 +37,7 @@ function ClassicEditorHost(props, ref) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
catch { }
|
|
40
|
-
}, placeholder: props.placeholder, minHeight: props.minHeight, maxHeight: props.maxHeight, readOnly: props.readOnly, table: props.table, media: props.media, formula: props.formula, mediaManager: props.mediaManager, theme: props.theme, className: props.className }) }));
|
|
40
|
+
}, placeholder: props.placeholder, minHeight: props.minHeight, maxHeight: props.maxHeight, readOnly: props.readOnly, table: props.table, media: props.media, formula: props.formula, showFontSize: props.showFontSize, mediaManager: props.mediaManager, theme: props.theme, className: props.className }) }));
|
|
41
41
|
}
|
|
42
42
|
const ClassicEditorHostWithRef = React.forwardRef(ClassicEditorHost);
|
|
43
43
|
function initClassicEditor(opts) {
|
package/dist/theme.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type SrteTheme = 'light' | 'dark';
|
|
2
|
-
export declare const SRTE_DEFAULT_CSS = "\n.srte-editor {\n --srte-bg: #ffffff;\n --srte-text: #111111;\n --srte-text-muted: #4b5563;\n --srte-border: #dddddd;\n --srte-border-light: #eeeeee;\n --srte-toolbar-bg: #ffffff;\n --srte-input-bg: #ffffff;\n --srte-input-text: #111111;\n --srte-input-border: #e5e7eb;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.35);\n --srte-modal-backdrop-filter: blur(2px);\n --srte-modal-bg: #ffffff;\n --srte-modal-text: #000000;\n --srte-menu-bg: #ffffff;\n --srte-menu-text: #111111;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n --srte-accent: #1e90ff;\n --srte-accent-bg: rgba(30, 144, 255, 0.15);\n --srte-danger: #dc2626;\n --srte-primary: #2563eb;\n --srte-surface-subtle: #f3f4f6;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #f3f4f6;\n}\n.srte-editor.srte-dark {\n --srte-bg: #1e1e1e;\n --srte-text: #e0e0e0;\n --srte-text-muted: #9ca3af;\n --srte-border: #3a3a3a;\n --srte-border-light: #2e2e2e;\n --srte-toolbar-bg: #252525;\n --srte-input-bg: #2a2a2a;\n --srte-input-text: #e0e0e0;\n --srte-input-border: #444444;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.22);\n --srte-modal-backdrop-filter: blur(10px) saturate(0.9);\n --srte-modal-bg: #1e293b;\n --srte-modal-text: #e0e0e0;\n --srte-menu-bg: #1e293b;\n --srte-menu-text: #e0e0e0;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);\n --srte-accent: #3b9eff;\n --srte-accent-bg: rgba(59, 158, 255, 0.2);\n --srte-danger: #ef4444;\n --srte-primary: #3b82f6;\n --srte-surface-subtle: #333333;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #333333;\n}\n.srte-editor [contenteditable] blockquote {\n border-left: 4px solid var(--srte-accent);\n margin: 0.75em 0;\n padding: 0.5em 1em;\n background: var(--srte-surface-subtle);\n color: var(--srte-text);\n}\n.srte-editor [contenteditable] ul {\n list-style-type: disc;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] ol {\n list-style-type: decimal;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] li {\n display: list-item;\n margin: 0.25em 0;\n padding-left: 0.25em;\n}\n.srte-editor [contenteditable] li::marker {\n color: currentColor;\n}\n.srte-editor.srte-dark [contenteditable] [style*=\"color\"]:not(.srte-preserve-colors):not(.srte-preserve-colors *),\n.srte-editor.srte-dark [contenteditable] [style*=\"background\"]:not(.srte-preserve-colors):not(.srte-preserve-colors *) {\n color: var(--srte-text) !important;\n background: transparent !important;\n background-color: transparent !important;\n}\n.srte-editor [contenteditable] sub,\n.srte-editor [contenteditable] sup {\n line-height: 0;\n}\n";
|
|
2
|
+
export declare const SRTE_DEFAULT_CSS = "\n.srte-editor {\n --srte-bg: #ffffff;\n --srte-text: #111111;\n --srte-text-muted: #4b5563;\n --srte-border: #dddddd;\n --srte-border-light: #eeeeee;\n --srte-toolbar-bg: #ffffff;\n --srte-input-bg: #ffffff;\n --srte-input-text: #111111;\n --srte-input-border: #e5e7eb;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.35);\n --srte-modal-backdrop-filter: blur(2px);\n --srte-modal-bg: #ffffff;\n --srte-modal-text: #000000;\n --srte-menu-bg: #ffffff;\n --srte-menu-text: #111111;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n --srte-accent: #1e90ff;\n --srte-accent-bg: rgba(30, 144, 255, 0.15);\n --srte-danger: #dc2626;\n --srte-primary: #2563eb;\n --srte-surface-subtle: #f3f4f6;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #f3f4f6;\n}\n.srte-editor.srte-dark {\n --srte-bg: #1e1e1e;\n --srte-text: #e0e0e0;\n --srte-text-muted: #9ca3af;\n --srte-border: #3a3a3a;\n --srte-border-light: #2e2e2e;\n --srte-toolbar-bg: #252525;\n --srte-input-bg: #2a2a2a;\n --srte-input-text: #e0e0e0;\n --srte-input-border: #444444;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.22);\n --srte-modal-backdrop-filter: blur(10px) saturate(0.9);\n --srte-modal-bg: #1e293b;\n --srte-modal-text: #e0e0e0;\n --srte-menu-bg: #1e293b;\n --srte-menu-text: #e0e0e0;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);\n --srte-accent: #3b9eff;\n --srte-accent-bg: rgba(59, 158, 255, 0.2);\n --srte-danger: #ef4444;\n --srte-primary: #3b82f6;\n --srte-surface-subtle: #333333;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #333333;\n}\n.srte-editor [contenteditable] blockquote {\n border-left: 4px solid var(--srte-accent);\n margin: 0.75em 0;\n padding: 0.5em 1em;\n background: var(--srte-surface-subtle);\n color: var(--srte-text);\n}\n.srte-editor [contenteditable] p,\n.srte-editor [contenteditable] h1,\n.srte-editor [contenteditable] h2,\n.srte-editor [contenteditable] h3 {\n color: inherit;\n}\n.srte-editor [contenteditable] p {\n display: block;\n margin: 0 0 0.75em;\n font-size: 1em;\n font-weight: 400;\n line-height: 1.6;\n}\n.srte-editor [contenteditable] h1,\n.srte-editor [contenteditable] h2,\n.srte-editor [contenteditable] h3 {\n display: block;\n margin: 0.75em 0 0.4em;\n font-weight: 700;\n line-height: 1.25;\n}\n.srte-editor [contenteditable] h1 {\n font-size: 2em;\n}\n.srte-editor [contenteditable] h2 {\n font-size: 1.5em;\n}\n.srte-editor [contenteditable] h3 {\n font-size: 1.25em;\n}\n.srte-editor [contenteditable] > :first-child {\n margin-top: 0;\n}\n.srte-editor [contenteditable] ul {\n list-style-type: disc;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] ol {\n list-style-type: decimal;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] li {\n display: list-item;\n margin: 0.25em 0;\n padding-left: 0.25em;\n}\n.srte-editor [contenteditable] li::marker {\n color: currentColor;\n}\n.srte-editor.srte-dark [contenteditable] [style*=\"color\"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *),\n.srte-editor.srte-dark [contenteditable] [style*=\"background\"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *) {\n color: var(--srte-text) !important;\n background: transparent !important;\n background-color: transparent !important;\n}\n.srte-editor [contenteditable] sub,\n.srte-editor [contenteditable] sup {\n line-height: 0;\n}\n";
|
|
3
3
|
export declare function ensureStyleSheet(): void;
|
package/dist/theme.js
CHANGED
|
@@ -56,6 +56,39 @@ export const SRTE_DEFAULT_CSS = `
|
|
|
56
56
|
background: var(--srte-surface-subtle);
|
|
57
57
|
color: var(--srte-text);
|
|
58
58
|
}
|
|
59
|
+
.srte-editor [contenteditable] p,
|
|
60
|
+
.srte-editor [contenteditable] h1,
|
|
61
|
+
.srte-editor [contenteditable] h2,
|
|
62
|
+
.srte-editor [contenteditable] h3 {
|
|
63
|
+
color: inherit;
|
|
64
|
+
}
|
|
65
|
+
.srte-editor [contenteditable] p {
|
|
66
|
+
display: block;
|
|
67
|
+
margin: 0 0 0.75em;
|
|
68
|
+
font-size: 1em;
|
|
69
|
+
font-weight: 400;
|
|
70
|
+
line-height: 1.6;
|
|
71
|
+
}
|
|
72
|
+
.srte-editor [contenteditable] h1,
|
|
73
|
+
.srte-editor [contenteditable] h2,
|
|
74
|
+
.srte-editor [contenteditable] h3 {
|
|
75
|
+
display: block;
|
|
76
|
+
margin: 0.75em 0 0.4em;
|
|
77
|
+
font-weight: 700;
|
|
78
|
+
line-height: 1.25;
|
|
79
|
+
}
|
|
80
|
+
.srte-editor [contenteditable] h1 {
|
|
81
|
+
font-size: 2em;
|
|
82
|
+
}
|
|
83
|
+
.srte-editor [contenteditable] h2 {
|
|
84
|
+
font-size: 1.5em;
|
|
85
|
+
}
|
|
86
|
+
.srte-editor [contenteditable] h3 {
|
|
87
|
+
font-size: 1.25em;
|
|
88
|
+
}
|
|
89
|
+
.srte-editor [contenteditable] > :first-child {
|
|
90
|
+
margin-top: 0;
|
|
91
|
+
}
|
|
59
92
|
.srte-editor [contenteditable] ul {
|
|
60
93
|
list-style-type: disc;
|
|
61
94
|
list-style-position: outside;
|
|
@@ -76,8 +109,8 @@ export const SRTE_DEFAULT_CSS = `
|
|
|
76
109
|
.srte-editor [contenteditable] li::marker {
|
|
77
110
|
color: currentColor;
|
|
78
111
|
}
|
|
79
|
-
.srte-editor.srte-dark [contenteditable] [style*="color"]:not(.srte-preserve-colors):not(.srte-preserve-colors *),
|
|
80
|
-
.srte-editor.srte-dark [contenteditable] [style*="background"]:not(.srte-preserve-colors):not(.srte-preserve-colors *) {
|
|
112
|
+
.srte-editor.srte-dark [contenteditable] [style*="color"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *),
|
|
113
|
+
.srte-editor.srte-dark [contenteditable] [style*="background"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *) {
|
|
81
114
|
color: var(--srte-text) !important;
|
|
82
115
|
background: transparent !important;
|
|
83
116
|
background-color: transparent !important;
|
|
@@ -91,8 +124,13 @@ const SRTE_STYLE_ID = 'srte-theme-defaults';
|
|
|
91
124
|
export function ensureStyleSheet() {
|
|
92
125
|
if (typeof document === 'undefined')
|
|
93
126
|
return;
|
|
94
|
-
|
|
127
|
+
const existing = document.getElementById(SRTE_STYLE_ID);
|
|
128
|
+
if (existing) {
|
|
129
|
+
if (existing.textContent !== SRTE_DEFAULT_CSS) {
|
|
130
|
+
existing.textContent = SRTE_DEFAULT_CSS;
|
|
131
|
+
}
|
|
95
132
|
return;
|
|
133
|
+
}
|
|
96
134
|
const style = document.createElement('style');
|
|
97
135
|
style.id = SRTE_STYLE_ID;
|
|
98
136
|
style.textContent = SRTE_DEFAULT_CSS;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smartrte-react",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
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",
|