smartrte-react 0.1.6 → 0.1.8

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.
@@ -6,9 +6,11 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
6
6
  const lastEmittedRef = useRef("");
7
7
  const isComposingRef = useRef(false);
8
8
  const fileInputRef = useRef(null);
9
+ const replaceTargetRef = useRef(null);
9
10
  const [selectedImage, setSelectedImage] = useState(null);
10
11
  const [imageOverlay, setImageOverlay] = useState(null);
11
12
  const resizingRef = useRef(null);
13
+ const draggedImageRef = useRef(null);
12
14
  const [showTableDialog, setShowTableDialog] = useState(false);
13
15
  const [tableRows, setTableRows] = useState(3);
14
16
  const [tableCols, setTableCols] = useState(3);
@@ -17,7 +19,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
17
19
  const [tableMenu, setTableMenu] = useState(null);
18
20
  const selectionRef = useRef(null);
19
21
  const selectingRef = useRef(null);
22
+ const [imageMenu, setImageMenu] = useState(null);
20
23
  const [showMediaManager, setShowMediaManager] = useState(false);
24
+ const [showColorPicker, setShowColorPicker] = useState(false);
25
+ const [colorPickerType, setColorPickerType] = useState('text');
26
+ const savedRangeRef = useRef(null);
27
+ const [currentFontSize, setCurrentFontSize] = useState("11");
21
28
  useEffect(() => {
22
29
  const el = editableRef.current;
23
30
  if (!el)
@@ -39,6 +46,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
39
46
  el.removeEventListener("contextmenu", onCtx, { capture: true });
40
47
  };
41
48
  }, [value]);
49
+ // Save selection whenever it changes
50
+ useEffect(() => {
51
+ const saveSelection = () => {
52
+ const sel = window.getSelection();
53
+ if (sel && sel.rangeCount > 0) {
54
+ const range = sel.getRangeAt(0);
55
+ const editor = editableRef.current;
56
+ if (editor && editor.contains(range.commonAncestorContainer)) {
57
+ savedRangeRef.current = range.cloneRange();
58
+ }
59
+ }
60
+ };
61
+ document.addEventListener('selectionchange', saveSelection);
62
+ return () => {
63
+ document.removeEventListener('selectionchange', saveSelection);
64
+ };
65
+ }, []);
42
66
  const exec = (command, valueArg) => {
43
67
  try {
44
68
  document.execCommand(command, false, valueArg);
@@ -63,6 +87,76 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
63
87
  return;
64
88
  exec("createLink", url);
65
89
  };
90
+ const applyFontSize = (size) => {
91
+ try {
92
+ // Update current font size state
93
+ setCurrentFontSize(size);
94
+ const editor = editableRef.current;
95
+ if (!editor)
96
+ return;
97
+ editor.focus();
98
+ // Try to get current selection, or use saved range
99
+ let range = null;
100
+ const sel = window.getSelection();
101
+ if (sel && sel.rangeCount > 0) {
102
+ const currentRange = sel.getRangeAt(0);
103
+ // Use current range if it's within our editor
104
+ if (editor.contains(currentRange.commonAncestorContainer)) {
105
+ range = currentRange;
106
+ }
107
+ }
108
+ // Fallback to saved range if current range is not available
109
+ if (!range && savedRangeRef.current) {
110
+ range = savedRangeRef.current.cloneRange();
111
+ }
112
+ // If no range at all, just update state for future typing
113
+ if (!range)
114
+ return;
115
+ // If range is collapsed (cursor position, no selection), insert an invisible span
116
+ if (range.collapsed) {
117
+ // Create a span with zero-width space that will capture future typing
118
+ const span = document.createElement('span');
119
+ span.style.fontSize = size + 'pt';
120
+ span.textContent = '\u200B'; // Zero-width space
121
+ range.insertNode(span);
122
+ // Position cursor inside the span
123
+ const newRange = document.createRange();
124
+ newRange.setStart(span.firstChild, 1);
125
+ newRange.collapse(true);
126
+ if (sel) {
127
+ sel.removeAllRanges();
128
+ sel.addRange(newRange);
129
+ }
130
+ handleInput();
131
+ return;
132
+ }
133
+ // If there's selected text, wrap it
134
+ const span = document.createElement('span');
135
+ span.style.fontSize = size + 'pt';
136
+ // Extract the selected content and wrap it in the span
137
+ const fragment = range.extractContents();
138
+ span.appendChild(fragment);
139
+ // Insert the span at the current position
140
+ range.insertNode(span);
141
+ // Update selection to show what was changed
142
+ if (sel) {
143
+ range.selectNodeContents(span);
144
+ sel.removeAllRanges();
145
+ sel.addRange(range);
146
+ }
147
+ // Trigger change event
148
+ handleInput();
149
+ }
150
+ catch (error) {
151
+ console.error('Error applying font size:', error);
152
+ }
153
+ };
154
+ const applyTextColor = (color) => {
155
+ exec("foreColor", color);
156
+ };
157
+ const applyBackgroundColor = (color) => {
158
+ exec("hiliteColor", color);
159
+ };
66
160
  const insertImage = () => {
67
161
  if (!media)
68
162
  return;
@@ -116,6 +210,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
116
210
  }
117
211
  const img = document.createElement("img");
118
212
  img.src = src;
213
+ img.draggable = true;
119
214
  img.style.maxWidth = "100%";
120
215
  img.style.height = "auto";
121
216
  img.style.display = "inline-block";
@@ -652,13 +747,36 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
652
747
  gap: 8,
653
748
  padding: 8,
654
749
  borderBottom: "1px solid #eee",
655
- background: "#fafafa",
750
+ background: "#fff",
751
+ color: "#111",
656
752
  position: "sticky",
657
753
  top: 0,
658
754
  zIndex: 1,
659
755
  }, children: [media && (_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: "none" }, onChange: (e) => {
660
- if (e.currentTarget.files)
661
- handleLocalImageFiles(e.currentTarget.files);
756
+ const list = e.currentTarget.files;
757
+ if (list && list.length) {
758
+ if (replaceTargetRef.current) {
759
+ const img = replaceTargetRef.current;
760
+ replaceTargetRef.current = null;
761
+ const f = list[0];
762
+ if (f && f.type.startsWith("image/")) {
763
+ const reader = new FileReader();
764
+ reader.onload = () => {
765
+ const dataUrl = String(reader.result || "");
766
+ if (dataUrl) {
767
+ img.src = dataUrl;
768
+ setSelectedImage(img);
769
+ scheduleImageOverlay();
770
+ handleInput();
771
+ }
772
+ };
773
+ reader.readAsDataURL(f);
774
+ }
775
+ }
776
+ else {
777
+ handleLocalImageFiles(list);
778
+ }
779
+ }
662
780
  e.currentTarget.value = "";
663
781
  } })), _jsxs("select", { defaultValue: "p", onChange: (e) => {
664
782
  const val = e.target.value;
@@ -670,7 +788,165 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
670
788
  applyFormatBlock("<h2>");
671
789
  else if (val === "h3")
672
790
  applyFormatBlock("<h3>");
673
- }, title: "Paragraph/Heading", 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", { onClick: () => exec("bold"), children: "B" }), _jsx("button", { onClick: () => exec("italic"), children: "I" }), _jsx("button", { onClick: () => exec("underline"), children: "U" }), _jsx("button", { onClick: () => exec("strikeThrough"), children: "S" }), _jsx("button", { onClick: () => exec("insertUnorderedList"), children: "\u2022 List" }), _jsx("button", { onClick: () => exec("insertOrderedList"), children: "1. List" }), _jsx("button", { onClick: () => exec("formatBlock", "<blockquote>"), children: "\u275D" }), _jsx("button", { onClick: () => exec("formatBlock", "<pre>"), children: "< />" }), formula && (_jsx("button", { title: "Formula", onClick: () => setShowFormulaDialog(true), children: "\u2211" })), _jsx("button", { onClick: insertLink, children: "Link" }), _jsx("button", { onClick: () => exec("unlink"), children: "Unlink" }), media && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: insertImage, children: "Image" }), mediaManager && (_jsx("button", { onClick: () => setShowMediaManager(true), children: "Media manager" })), _jsxs("div", { style: {
791
+ }, title: "Paragraph/Heading", style: {
792
+ height: 32,
793
+ padding: "0 8px",
794
+ border: "1px solid #e5e7eb",
795
+ borderRadius: 6,
796
+ background: "#fff",
797
+ color: "#111",
798
+ }, 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"), style: {
799
+ height: 32,
800
+ minWidth: 32,
801
+ padding: "0 8px",
802
+ border: "1px solid #e5e7eb",
803
+ borderRadius: 6,
804
+ background: "#fff",
805
+ color: "#111",
806
+ }, children: _jsx("span", { style: { fontWeight: 700 }, children: "B" }) }), _jsx("button", { title: "Italic", onClick: () => exec("italic"), style: {
807
+ height: 32,
808
+ minWidth: 32,
809
+ padding: "0 8px",
810
+ border: "1px solid #e5e7eb",
811
+ borderRadius: 6,
812
+ background: "#fff",
813
+ fontStyle: "italic",
814
+ color: "#111",
815
+ }, children: "I" }), _jsx("button", { title: "Underline", onClick: () => exec("underline"), style: {
816
+ height: 32,
817
+ minWidth: 32,
818
+ padding: "0 8px",
819
+ border: "1px solid #e5e7eb",
820
+ borderRadius: 6,
821
+ background: "#fff",
822
+ textDecoration: "underline",
823
+ color: "#111",
824
+ }, children: "U" }), _jsx("button", { title: "Strikethrough", onClick: () => exec("strikeThrough"), style: {
825
+ height: 32,
826
+ minWidth: 32,
827
+ padding: "0 8px",
828
+ border: "1px solid #e5e7eb",
829
+ borderRadius: 6,
830
+ background: "#fff",
831
+ textDecoration: "line-through",
832
+ color: "#111",
833
+ }, children: "S" }), _jsxs("select", { value: currentFontSize, onMouseDown: () => {
834
+ // Save selection before dropdown interaction
835
+ const sel = window.getSelection();
836
+ if (sel && sel.rangeCount > 0) {
837
+ const range = sel.getRangeAt(0);
838
+ const editor = editableRef.current;
839
+ if (editor && editor.contains(range.commonAncestorContainer) && !range.collapsed) {
840
+ savedRangeRef.current = range.cloneRange();
841
+ }
842
+ }
843
+ }, onChange: (e) => applyFontSize(e.target.value), title: "Font Size", style: {
844
+ height: 32,
845
+ padding: "0 8px",
846
+ border: "1px solid #e5e7eb",
847
+ borderRadius: 6,
848
+ background: "#fff",
849
+ color: "#111",
850
+ }, children: [_jsx("option", { value: "8", children: "8" }), _jsx("option", { value: "9", children: "9" }), _jsx("option", { value: "10", children: "10" }), _jsx("option", { value: "11", children: "11" }), _jsx("option", { value: "12", children: "12" }), _jsx("option", { value: "14", children: "14" }), _jsx("option", { value: "18", children: "18" }), _jsx("option", { value: "24", children: "24" }), _jsx("option", { value: "30", children: "30" }), _jsx("option", { value: "36", children: "36" }), _jsx("option", { value: "48", children: "48" }), _jsx("option", { value: "60", children: "60" }), _jsx("option", { value: "72", children: "72" }), _jsx("option", { value: "96", children: "96" })] }), _jsxs("button", { title: "Text Color", onClick: () => {
851
+ setColorPickerType('text');
852
+ setShowColorPicker(true);
853
+ }, style: {
854
+ height: 32,
855
+ minWidth: 32,
856
+ padding: "0 8px",
857
+ border: "1px solid #e5e7eb",
858
+ borderRadius: 6,
859
+ background: "#fff",
860
+ color: "#111",
861
+ position: "relative",
862
+ }, children: [_jsx("span", { style: { fontWeight: 700 }, children: "A" }), _jsx("div", { style: {
863
+ position: "absolute",
864
+ bottom: 4,
865
+ left: "50%",
866
+ transform: "translateX(-50%)",
867
+ width: 16,
868
+ height: 3,
869
+ background: "#000",
870
+ borderRadius: 1,
871
+ } })] }), _jsx("button", { title: "Background Color", onClick: () => {
872
+ setColorPickerType('background');
873
+ setShowColorPicker(true);
874
+ }, style: {
875
+ height: 32,
876
+ minWidth: 32,
877
+ padding: "0 8px",
878
+ border: "1px solid #e5e7eb",
879
+ borderRadius: 6,
880
+ background: "#fff",
881
+ color: "#111",
882
+ }, children: _jsx("span", { style: { fontWeight: 700, background: "#ffeb3b", padding: "2px 4px" }, children: "A" }) }), _jsx("button", { title: "Bulleted list", onClick: () => exec("insertUnorderedList"), style: {
883
+ height: 32,
884
+ padding: "0 10px",
885
+ border: "1px solid #e5e7eb",
886
+ borderRadius: 6,
887
+ background: "#fff",
888
+ color: "#111",
889
+ }, children: "\u2022 List" }), _jsx("button", { title: "Numbered list", onClick: () => exec("insertOrderedList"), style: {
890
+ height: 32,
891
+ padding: "0 10px",
892
+ border: "1px solid #e5e7eb",
893
+ borderRadius: 6,
894
+ background: "#fff",
895
+ color: "#111",
896
+ }, children: "1. List" }), _jsx("button", { title: "Blockquote", onClick: () => exec("formatBlock", "<blockquote>"), style: {
897
+ height: 32,
898
+ minWidth: 32,
899
+ padding: "0 8px",
900
+ border: "1px solid #e5e7eb",
901
+ borderRadius: 6,
902
+ background: "#fff",
903
+ color: "#111",
904
+ }, children: "\u275D" }), _jsx("button", { title: "Code block", onClick: () => exec("formatBlock", "<pre>"), style: {
905
+ height: 32,
906
+ minWidth: 36,
907
+ padding: "0 8px",
908
+ border: "1px solid #e5e7eb",
909
+ borderRadius: 6,
910
+ background: "#fff",
911
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo",
912
+ color: "#111",
913
+ }, children: "< />" }), formula && (_jsx("button", { title: "Insert formula", onClick: () => setShowFormulaDialog(true), style: {
914
+ height: 32,
915
+ minWidth: 32,
916
+ padding: "0 8px",
917
+ border: "1px solid #e5e7eb",
918
+ borderRadius: 6,
919
+ background: "#fff",
920
+ color: "#111",
921
+ }, children: "\u2211" })), _jsx("button", { title: "Insert link", onClick: insertLink, style: {
922
+ height: 32,
923
+ padding: "0 10px",
924
+ border: "1px solid #e5e7eb",
925
+ borderRadius: 6,
926
+ background: "#fff",
927
+ color: "#111",
928
+ }, children: "Link" }), _jsx("button", { title: "Remove link", onClick: () => exec("unlink"), style: {
929
+ height: 32,
930
+ padding: "0 10px",
931
+ border: "1px solid #e5e7eb",
932
+ borderRadius: 6,
933
+ background: "#fff",
934
+ color: "#111",
935
+ }, children: "Unlink" }), media && (_jsxs(_Fragment, { children: [_jsx("button", { title: "Insert image", onClick: insertImage, style: {
936
+ height: 32,
937
+ padding: "0 10px",
938
+ border: "1px solid #e5e7eb",
939
+ borderRadius: 6,
940
+ background: "#fff",
941
+ color: "#111",
942
+ }, children: "\uD83D\uDDBC\uFE0F Image" }), mediaManager && (_jsx("button", { title: "Open media manager", onClick: () => setShowMediaManager(true), style: {
943
+ height: 32,
944
+ padding: "0 10px",
945
+ border: "1px solid #e5e7eb",
946
+ borderRadius: 6,
947
+ background: "#fff",
948
+ color: "#111",
949
+ }, children: "\uD83D\uDCC1 Media" })), _jsxs("div", { style: {
674
950
  display: "inline-flex",
675
951
  gap: 4,
676
952
  alignItems: "center",
@@ -684,7 +960,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
684
960
  img.style.float = "none";
685
961
  scheduleImageOverlay();
686
962
  handleInput();
687
- }, title: "Center", children: "\u2299" }), _jsx("button", { onClick: () => {
963
+ }, title: "Center", style: {
964
+ height: 28,
965
+ minWidth: 28,
966
+ padding: "0 6px",
967
+ border: "1px solid #e5e7eb",
968
+ borderRadius: 6,
969
+ background: "#fff",
970
+ color: "#111",
971
+ }, children: "\u2299" }), _jsx("button", { onClick: () => {
688
972
  const img = selectedImage;
689
973
  if (!img)
690
974
  return;
@@ -693,7 +977,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
693
977
  img.style.margin = "0 8px 8px 0";
694
978
  scheduleImageOverlay();
695
979
  handleInput();
696
- }, title: "Float left", children: "\u27F8" }), _jsx("button", { onClick: () => {
980
+ }, title: "Float left", style: {
981
+ height: 28,
982
+ minWidth: 28,
983
+ padding: "0 6px",
984
+ border: "1px solid #e5e7eb",
985
+ borderRadius: 6,
986
+ background: "#fff",
987
+ color: "#111",
988
+ }, children: "\u27F8" }), _jsx("button", { onClick: () => {
697
989
  const img = selectedImage;
698
990
  if (!img)
699
991
  return;
@@ -702,7 +994,36 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
702
994
  img.style.margin = "0 0 8px 8px";
703
995
  scheduleImageOverlay();
704
996
  handleInput();
705
- }, title: "Float right", children: "\u27F9" })] })] })), table && (_jsx("button", { onClick: () => setShowTableDialog(true), children: "+ Table" })), _jsx("button", { onClick: () => exec("undo"), children: "Undo" }), _jsx("button", { onClick: () => exec("redo"), children: "Redo" })] }), media && mediaManager && (_jsx(MediaManager, { open: showMediaManager, onClose: () => setShowMediaManager(false), adapter: mediaManager, onSelect: (item) => {
997
+ }, title: "Float right", style: {
998
+ height: 28,
999
+ minWidth: 28,
1000
+ padding: "0 6px",
1001
+ border: "1px solid #e5e7eb",
1002
+ borderRadius: 6,
1003
+ background: "#fff",
1004
+ color: "#111",
1005
+ }, children: "\u27F9" })] })] })), table && (_jsx("button", { title: "Insert table", onClick: () => setShowTableDialog(true), style: {
1006
+ height: 32,
1007
+ padding: "0 10px",
1008
+ border: "1px solid #e5e7eb",
1009
+ borderRadius: 6,
1010
+ background: "#fff",
1011
+ color: "#111",
1012
+ }, children: "\u2795 Table" })), _jsx("button", { title: "Undo", onClick: () => exec("undo"), style: {
1013
+ height: 32,
1014
+ padding: "0 10px",
1015
+ border: "1px solid #e5e7eb",
1016
+ borderRadius: 6,
1017
+ background: "#fff",
1018
+ color: "#111",
1019
+ }, children: "\u238C Undo" }), _jsx("button", { title: "Redo", onClick: () => exec("redo"), style: {
1020
+ height: 32,
1021
+ padding: "0 10px",
1022
+ border: "1px solid #e5e7eb",
1023
+ borderRadius: 6,
1024
+ background: "#fff",
1025
+ color: "#111",
1026
+ }, children: "\u293E Redo" })] }), media && mediaManager && (_jsx(MediaManager, { open: showMediaManager, onClose: () => setShowMediaManager(false), adapter: mediaManager, onSelect: (item) => {
706
1027
  if (item?.url)
707
1028
  insertImageAtSelection(item.url);
708
1029
  } })), table && showTableDialog && (_jsx("div", { style: {
@@ -748,7 +1069,72 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
748
1069
  }, children: [_jsx("button", { onClick: () => setShowTableDialog(false), children: "Cancel" }), _jsx("button", { onClick: () => {
749
1070
  insertTable();
750
1071
  setShowTableDialog(false);
751
- }, children: "Insert" })] })] }) })), formula && showFormulaDialog && (_jsx("div", { style: {
1072
+ }, children: "Insert" })] })] }) })), showColorPicker && (_jsx("div", { style: {
1073
+ position: "fixed",
1074
+ inset: 0,
1075
+ background: "rgba(0,0,0,0.35)",
1076
+ display: "flex",
1077
+ alignItems: "center",
1078
+ justifyContent: "center",
1079
+ zIndex: 50,
1080
+ }, onClick: () => setShowColorPicker(false), children: _jsxs("div", { style: {
1081
+ background: "#fff",
1082
+ padding: 16,
1083
+ borderRadius: 8,
1084
+ minWidth: 320,
1085
+ color: "#000",
1086
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 12 }, children: colorPickerType === 'text' ? 'Select Text Color' : 'Select Background Color' }), _jsx("div", { style: {
1087
+ display: "grid",
1088
+ gridTemplateColumns: "repeat(8, 1fr)",
1089
+ gap: 8,
1090
+ marginBottom: 12,
1091
+ }, children: [
1092
+ '#000000', '#434343', '#666666', '#999999',
1093
+ '#b7b7b7', '#cccccc', '#d9d9d9', '#efefef',
1094
+ '#f3f3f3', '#ffffff', '#980000', '#ff0000',
1095
+ '#ff9900', '#ffff00', '#00ff00', '#00ffff',
1096
+ '#4a86e8', '#0000ff', '#9900ff', '#ff00ff',
1097
+ '#e6b8af', '#f4cccc', '#fce5cd', '#fff2cc',
1098
+ '#d9ead3', '#d0e0e3', '#c9daf8', '#cfe2f3',
1099
+ '#d9d2e9', '#ead1dc', '#dd7e6b', '#ea9999',
1100
+ '#f9cb9c', '#ffe599', '#b6d7a8', '#a2c4c9',
1101
+ '#a4c2f4', '#9fc5e8', '#b4a7d6', '#d5a6bd',
1102
+ ].map((color) => (_jsx("button", { onClick: () => {
1103
+ if (colorPickerType === 'text') {
1104
+ applyTextColor(color);
1105
+ }
1106
+ else {
1107
+ applyBackgroundColor(color);
1108
+ }
1109
+ setShowColorPicker(false);
1110
+ }, style: {
1111
+ width: 32,
1112
+ height: 32,
1113
+ border: color === '#ffffff' ? '1px solid #ddd' : 'none',
1114
+ borderRadius: 4,
1115
+ background: color,
1116
+ cursor: 'pointer',
1117
+ }, 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) => {
1118
+ if (colorPickerType === 'text') {
1119
+ applyTextColor(e.target.value);
1120
+ }
1121
+ else {
1122
+ applyBackgroundColor(e.target.value);
1123
+ }
1124
+ }, style: {
1125
+ width: '100%',
1126
+ height: 40,
1127
+ border: '1px solid #ddd',
1128
+ borderRadius: 6,
1129
+ cursor: 'pointer',
1130
+ } })] }), _jsx("div", { style: { display: 'flex', justifyContent: 'end' }, children: _jsx("button", { onClick: () => setShowColorPicker(false), style: {
1131
+ padding: '6px 16px',
1132
+ border: '1px solid #e5e7eb',
1133
+ borderRadius: 6,
1134
+ background: '#fff',
1135
+ color: '#111',
1136
+ cursor: 'pointer',
1137
+ }, children: "Close" }) })] }) })), formula && showFormulaDialog && (_jsx("div", { style: {
752
1138
  position: "fixed",
753
1139
  inset: 0,
754
1140
  background: "rgba(0,0,0,0.35)",
@@ -862,10 +1248,65 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
862
1248
  }
863
1249
  }
864
1250
  }, onDragOver: (e) => {
865
- if (e.dataTransfer?.types?.includes("Files")) {
1251
+ // Allow dragging images within editor and file drops
1252
+ if (draggedImageRef.current ||
1253
+ e.dataTransfer?.types?.includes("Files")) {
866
1254
  e.preventDefault();
867
1255
  }
868
1256
  }, onDrop: (e) => {
1257
+ // Move existing dragged image inside editor
1258
+ if (draggedImageRef.current) {
1259
+ e.preventDefault();
1260
+ const x = e.clientX;
1261
+ const y = e.clientY;
1262
+ let range = null;
1263
+ // @ts-ignore
1264
+ if (document.caretRangeFromPoint) {
1265
+ // @ts-ignore
1266
+ range = document.caretRangeFromPoint(x, y);
1267
+ }
1268
+ else if (document.caretPositionFromPoint) {
1269
+ const pos = document.caretPositionFromPoint(x, y);
1270
+ if (pos) {
1271
+ range = document.createRange();
1272
+ range.setStart(pos.offsetNode, pos.offset);
1273
+ }
1274
+ }
1275
+ const img = draggedImageRef.current;
1276
+ draggedImageRef.current = null;
1277
+ if (range &&
1278
+ img &&
1279
+ editableRef.current?.contains(range.commonAncestorContainer)) {
1280
+ // Avoid inserting inside the image itself
1281
+ if (range.startContainer === img || range.endContainer === img)
1282
+ return;
1283
+ // If dropping inside a link, insert right after the link element
1284
+ let container = range.commonAncestorContainer;
1285
+ let linkAncestor = null;
1286
+ let el = container;
1287
+ while (el && el !== editableRef.current) {
1288
+ if (el.tagName === "A") {
1289
+ linkAncestor = el;
1290
+ break;
1291
+ }
1292
+ el = el.parentElement;
1293
+ }
1294
+ if (linkAncestor) {
1295
+ linkAncestor.parentElement?.insertBefore(img, linkAncestor.nextSibling);
1296
+ }
1297
+ else {
1298
+ range.insertNode(img);
1299
+ }
1300
+ const r = document.createRange();
1301
+ r.setStartAfter(img);
1302
+ r.collapse(true);
1303
+ safeSelectRange(r);
1304
+ setSelectedImage(img);
1305
+ scheduleImageOverlay();
1306
+ handleInput();
1307
+ }
1308
+ return;
1309
+ }
869
1310
  if (media && e.dataTransfer?.files?.length) {
870
1311
  e.preventDefault();
871
1312
  // Try to move caret to drop point
@@ -901,6 +1342,30 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
901
1342
  setSelectedImage(null);
902
1343
  setImageOverlay(null);
903
1344
  }
1345
+ }, onDragStart: (e) => {
1346
+ const t = e.target;
1347
+ if (t && t.tagName === "IMG") {
1348
+ draggedImageRef.current = t;
1349
+ try {
1350
+ e.dataTransfer?.setData("text/plain", "moving-image");
1351
+ e.dataTransfer.effectAllowed = "move";
1352
+ // Provide a subtle drag image
1353
+ const dt = e.dataTransfer;
1354
+ if (dt && typeof dt.setDragImage === "function") {
1355
+ const ghost = new Image();
1356
+ ghost.src = t.src;
1357
+ ghost.width = Math.min(120, t.width);
1358
+ ghost.height = Math.min(120, t.height);
1359
+ dt.setDragImage(ghost, 10, 10);
1360
+ }
1361
+ }
1362
+ catch { }
1363
+ }
1364
+ else {
1365
+ draggedImageRef.current = null;
1366
+ }
1367
+ }, onDragEnd: () => {
1368
+ draggedImageRef.current = null;
904
1369
  }, style: {
905
1370
  padding: 12,
906
1371
  minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
@@ -1008,6 +1473,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1008
1473
  window.addEventListener("mousemove", onMove);
1009
1474
  window.addEventListener("mouseup", onUp);
1010
1475
  }, onContextMenu: (e) => {
1476
+ const target = e.target;
1477
+ if (target && target.tagName === "IMG") {
1478
+ e.preventDefault();
1479
+ const vw = window.innerWidth;
1480
+ const vh = window.innerHeight;
1481
+ const menuW = 220;
1482
+ const menuH = 200;
1483
+ const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
1484
+ const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
1485
+ setImageMenu({ x, y, img: target });
1486
+ setTableMenu(null);
1487
+ return;
1488
+ }
1011
1489
  const cell = getClosestCell(e.target);
1012
1490
  if (cell) {
1013
1491
  e.preventDefault();
@@ -1021,6 +1499,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1021
1499
  }
1022
1500
  else {
1023
1501
  setTableMenu(null);
1502
+ setImageMenu(null);
1024
1503
  }
1025
1504
  } }), selectedImage && imageOverlay && (_jsxs("div", { style: {
1026
1505
  position: "fixed",
@@ -1029,7 +1508,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1029
1508
  width: imageOverlay.width,
1030
1509
  height: imageOverlay.height,
1031
1510
  pointerEvents: "none",
1032
- zIndex: 55,
1511
+ zIndex: 49,
1033
1512
  }, children: [_jsx("div", { style: {
1034
1513
  position: "absolute",
1035
1514
  inset: 0,
@@ -1145,6 +1624,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1145
1624
  width: 200,
1146
1625
  maxHeight: 260,
1147
1626
  overflowY: "auto",
1627
+ color: "#111",
1148
1628
  }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, fontSize: 11, margin: "2px 6px 6px" }, children: "Table" }), _jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsxs("button", { style: {
1149
1629
  display: "flex",
1150
1630
  alignItems: "center",
@@ -1276,5 +1756,143 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1276
1756
  }, onClick: () => {
1277
1757
  deleteTable(tableMenu.cell);
1278
1758
  setTableMenu(null);
1279
- }, children: [_jsx("span", { children: "\uD83D\uDDD1" }), _jsx("span", { children: "Delete table" })] })] })] }) }))] }));
1759
+ }, children: [_jsx("span", { children: "\uD83D\uDDD1" }), _jsx("span", { children: "Delete table" })] })] })] }) })), imageMenu && (_jsx("div", { style: { position: "fixed", inset: 0, zIndex: 60 }, onClick: () => setImageMenu(null), onContextMenu: (e) => {
1760
+ e.preventDefault();
1761
+ const vw = window.innerWidth;
1762
+ const vh = window.innerHeight;
1763
+ const menuW = 220;
1764
+ const menuH = 220;
1765
+ const x = Math.max(8, Math.min(e.clientX, vw - menuW - 8));
1766
+ const y = Math.max(8, Math.min(e.clientY, vh - menuH - 8));
1767
+ setImageMenu({ x, y, img: imageMenu.img });
1768
+ }, children: _jsxs("div", { style: {
1769
+ position: "fixed",
1770
+ left: imageMenu.x,
1771
+ top: imageMenu.y,
1772
+ background: "#fff",
1773
+ border: "1px solid #ddd",
1774
+ borderRadius: 8,
1775
+ boxShadow: "0 8px 24px rgba(0,0,0,0.18)",
1776
+ padding: 8,
1777
+ width: 220,
1778
+ color: "#111",
1779
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontWeight: 600, fontSize: 11, margin: "2px 6px 6px" }, children: "Image" }), _jsxs("div", { style: { display: "grid", gap: 6 }, children: [_jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [_jsx("span", { style: { width: 48, fontSize: 12 }, children: "Link" }), _jsx("input", { defaultValue: imageMenu.img.parentElement?.tagName === "A"
1780
+ ? imageMenu.img.parentElement.href
1781
+ : "", placeholder: "https://", onChange: (e) => {
1782
+ const url = e.target.value.trim();
1783
+ const curParent = imageMenu.img.parentElement;
1784
+ if (url) {
1785
+ if (curParent && curParent.tagName === "A") {
1786
+ curParent.href = url;
1787
+ }
1788
+ else {
1789
+ const a = document.createElement("a");
1790
+ a.href = url;
1791
+ curParent?.insertBefore(a, imageMenu.img);
1792
+ a.appendChild(imageMenu.img);
1793
+ }
1794
+ }
1795
+ else if (curParent && curParent.tagName === "A") {
1796
+ // unwrap
1797
+ curParent.parentElement?.insertBefore(imageMenu.img, curParent);
1798
+ curParent.remove();
1799
+ }
1800
+ handleInput();
1801
+ }, style: {
1802
+ flex: 1,
1803
+ padding: "4px 6px",
1804
+ border: "1px solid #eee",
1805
+ borderRadius: 4,
1806
+ color: "#111",
1807
+ background: "#fff",
1808
+ } })] }), _jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [_jsx("span", { style: { width: 48, fontSize: 12 }, children: "Target" }), _jsxs("select", { defaultValue: imageMenu.img.parentElement?.tagName === "A"
1809
+ ? imageMenu.img.parentElement
1810
+ .target || "_self"
1811
+ : "_self", onChange: (e) => {
1812
+ const curParent = imageMenu.img.parentElement;
1813
+ if (curParent && curParent.tagName === "A") {
1814
+ curParent.target = e.target.value;
1815
+ handleInput();
1816
+ }
1817
+ }, style: {
1818
+ flex: 1,
1819
+ height: 28,
1820
+ padding: "0 6px",
1821
+ border: "1px solid #eee",
1822
+ borderRadius: 4,
1823
+ background: "#fff",
1824
+ color: "#111",
1825
+ }, children: [_jsx("option", { value: "_self", children: "Same tab" }), _jsx("option", { value: "_blank", children: "New tab" })] })] }), _jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [_jsx("span", { style: { width: 48, fontSize: 12 }, children: "Alt" }), _jsx("input", { defaultValue: imageMenu.img.alt || "", onChange: (e) => {
1826
+ imageMenu.img.alt = e.target.value;
1827
+ handleInput();
1828
+ }, style: {
1829
+ flex: 1,
1830
+ padding: "4px 6px",
1831
+ border: "1px solid #eee",
1832
+ borderRadius: 4,
1833
+ color: "#111",
1834
+ background: "#fff",
1835
+ } })] }), _jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [_jsx("span", { style: { width: 48, fontSize: 12 }, children: "Width" }), _jsx("input", { type: "number", min: 40, max: 2000, defaultValue: Math.round(imageMenu.img.getBoundingClientRect().width), onChange: (e) => {
1836
+ const v = Math.max(40, Math.min(2000, Number(e.target.value) || 0));
1837
+ imageMenu.img.style.width = v + "px";
1838
+ imageMenu.img.style.height = "auto";
1839
+ scheduleImageOverlay();
1840
+ }, style: {
1841
+ width: 90,
1842
+ padding: "4px 6px",
1843
+ border: "1px solid #eee",
1844
+ borderRadius: 4,
1845
+ color: "#111",
1846
+ background: "#fff",
1847
+ } }), _jsx("span", { style: { fontSize: 12 }, children: "px" })] }), _jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [_jsx("span", { style: { width: 48, fontSize: 12 }, children: "Radius" }), _jsx("input", { type: "number", min: 0, max: 200, defaultValue: parseInt((imageMenu.img.style.borderRadius || "0").toString()) || 0, onChange: (e) => {
1848
+ const v = Math.max(0, Math.min(200, Number(e.target.value) || 0));
1849
+ imageMenu.img.style.borderRadius = v + "px";
1850
+ handleInput();
1851
+ }, style: {
1852
+ width: 90,
1853
+ padding: "4px 6px",
1854
+ border: "1px solid #eee",
1855
+ borderRadius: 4,
1856
+ color: "#111",
1857
+ background: "#fff",
1858
+ } }), _jsx("span", { style: { fontSize: 12 }, children: "px" })] }), _jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [_jsx("span", { style: { width: 48, fontSize: 12 }, children: "Align" }), _jsx("button", { onClick: () => {
1859
+ const img = imageMenu.img;
1860
+ img.style.display = "block";
1861
+ img.style.margin = "0 auto";
1862
+ img.style.float = "none";
1863
+ scheduleImageOverlay();
1864
+ handleInput();
1865
+ }, children: "\u29BF" }), _jsx("button", { onClick: () => {
1866
+ const img = imageMenu.img;
1867
+ img.style.display = "inline";
1868
+ img.style.float = "left";
1869
+ img.style.margin = "0 8px 8px 0";
1870
+ scheduleImageOverlay();
1871
+ handleInput();
1872
+ }, children: "\u27F8" }), _jsx("button", { onClick: () => {
1873
+ const img = imageMenu.img;
1874
+ img.style.display = "inline";
1875
+ img.style.float = "right";
1876
+ img.style.margin = "0 0 8px 8px";
1877
+ scheduleImageOverlay();
1878
+ handleInput();
1879
+ }, children: "\u27F9" })] }), _jsxs("div", { style: { display: "flex", gap: 6 }, children: [_jsx("button", { onClick: () => {
1880
+ replaceTargetRef.current = imageMenu.img;
1881
+ fileInputRef.current?.click();
1882
+ }, children: "Replace\u2026" }), _jsx("button", { onClick: () => {
1883
+ const img = imageMenu.img;
1884
+ img.style.width = "";
1885
+ img.style.height = "auto";
1886
+ img.style.borderRadius = "";
1887
+ img.style.margin = "";
1888
+ img.style.float = "none";
1889
+ scheduleImageOverlay();
1890
+ handleInput();
1891
+ }, children: "Reset" })] }), _jsxs("div", { style: { display: "flex", gap: 6 }, children: [_jsx("button", { style: { color: "#b00020" }, onClick: () => {
1892
+ imageMenu.img.remove();
1893
+ setImageMenu(null);
1894
+ setSelectedImage(null);
1895
+ setImageOverlay(null);
1896
+ handleInput();
1897
+ }, children: "Delete" }), _jsx("button", { onClick: () => setImageMenu(null), children: "Close" })] })] })] }) }))] }));
1280
1898
  }