smartrte-react 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { MediaManagerAdapter } from "./MediaManager";
1
2
  type ClassicEditorProps = {
2
3
  value?: string;
3
4
  onChange?: (html: string) => void;
@@ -5,6 +6,10 @@ type ClassicEditorProps = {
5
6
  minHeight?: number | string;
6
7
  maxHeight?: number | string;
7
8
  readOnly?: boolean;
9
+ table?: boolean;
10
+ media?: boolean;
11
+ formula?: boolean;
12
+ mediaManager?: MediaManagerAdapter;
8
13
  };
9
- export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, table, media, formula, mediaManager, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
10
15
  export {};
@@ -1,6 +1,7 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
- export function ClassicEditor({ value, onChange, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, }) {
3
+ import { MediaManager } from "./MediaManager";
4
+ export function ClassicEditor({ value, onChange, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, table = true, media = true, formula = true, mediaManager, }) {
4
5
  const editableRef = useRef(null);
5
6
  const lastEmittedRef = useRef("");
6
7
  const isComposingRef = useRef(false);
@@ -16,6 +17,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
16
17
  const [tableMenu, setTableMenu] = useState(null);
17
18
  const selectionRef = useRef(null);
18
19
  const selectingRef = useRef(null);
20
+ const [showMediaManager, setShowMediaManager] = useState(false);
19
21
  useEffect(() => {
20
22
  const el = editableRef.current;
21
23
  if (!el)
@@ -62,6 +64,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
62
64
  exec("createLink", url);
63
65
  };
64
66
  const insertImage = () => {
67
+ if (!media)
68
+ return;
65
69
  fileInputRef.current?.click();
66
70
  };
67
71
  const scheduleImageOverlay = () => {
@@ -148,6 +152,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
148
152
  const insertFormulaAtSelection = (tex) => {
149
153
  if (!tex)
150
154
  return;
155
+ if (!formula)
156
+ return;
151
157
  try {
152
158
  const host = editableRef.current;
153
159
  if (!host)
@@ -229,6 +235,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
229
235
  return s;
230
236
  };
231
237
  const handleLocalImageFiles = async (files) => {
238
+ if (!media)
239
+ return;
232
240
  const list = Array.from(files).filter((f) => f.type.startsWith("image/"));
233
241
  for (const f of list) {
234
242
  await new Promise((resolve) => {
@@ -648,11 +656,11 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
648
656
  position: "sticky",
649
657
  top: 0,
650
658
  zIndex: 1,
651
- }, children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: "none" }, onChange: (e) => {
659
+ }, children: [media && (_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: "none" }, onChange: (e) => {
652
660
  if (e.currentTarget.files)
653
661
  handleLocalImageFiles(e.currentTarget.files);
654
662
  e.currentTarget.value = "";
655
- } }), _jsxs("select", { defaultValue: "p", onChange: (e) => {
663
+ } })), _jsxs("select", { defaultValue: "p", onChange: (e) => {
656
664
  const val = e.target.value;
657
665
  if (val === "p")
658
666
  applyFormatBlock("<p>");
@@ -662,39 +670,42 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
662
670
  applyFormatBlock("<h2>");
663
671
  else if (val === "h3")
664
672
  applyFormatBlock("<h3>");
665
- }, 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: "< />" }), _jsx("button", { title: "Formula", onClick: () => setShowFormulaDialog(true), children: "\u2211" }), _jsx("button", { onClick: insertLink, children: "Link" }), _jsx("button", { onClick: () => exec("unlink"), children: "Unlink" }), _jsx("button", { onClick: insertImage, children: "Image" }), _jsxs("div", { style: {
666
- display: "inline-flex",
667
- gap: 4,
668
- alignItems: "center",
669
- marginLeft: 6,
670
- }, children: [_jsx("span", { style: { fontSize: 12, opacity: 0.7 }, children: "Image align:" }), _jsx("button", { onClick: () => {
671
- const img = selectedImage;
672
- if (!img)
673
- return;
674
- img.style.display = "block";
675
- img.style.margin = "0 auto";
676
- img.style.float = "none";
677
- scheduleImageOverlay();
678
- handleInput();
679
- }, title: "Center", children: "\u2299" }), _jsx("button", { onClick: () => {
680
- const img = selectedImage;
681
- if (!img)
682
- return;
683
- img.style.display = "inline";
684
- img.style.float = "left";
685
- img.style.margin = "0 8px 8px 0";
686
- scheduleImageOverlay();
687
- handleInput();
688
- }, title: "Float left", children: "\u27F8" }), _jsx("button", { onClick: () => {
689
- const img = selectedImage;
690
- if (!img)
691
- return;
692
- img.style.display = "inline";
693
- img.style.float = "right";
694
- img.style.margin = "0 0 8px 8px";
695
- scheduleImageOverlay();
696
- handleInput();
697
- }, title: "Float right", children: "\u27F9" })] }), _jsx("button", { onClick: () => setShowTableDialog(true), children: "+ Table" }), _jsx("button", { onClick: () => exec("undo"), children: "Undo" }), _jsx("button", { onClick: () => exec("redo"), children: "Redo" })] }), showTableDialog && (_jsx("div", { style: {
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: {
674
+ display: "inline-flex",
675
+ gap: 4,
676
+ alignItems: "center",
677
+ marginLeft: 6,
678
+ }, children: [_jsx("span", { style: { fontSize: 12, opacity: 0.7 }, children: "Image align:" }), _jsx("button", { onClick: () => {
679
+ const img = selectedImage;
680
+ if (!img)
681
+ return;
682
+ img.style.display = "block";
683
+ img.style.margin = "0 auto";
684
+ img.style.float = "none";
685
+ scheduleImageOverlay();
686
+ handleInput();
687
+ }, title: "Center", children: "\u2299" }), _jsx("button", { onClick: () => {
688
+ const img = selectedImage;
689
+ if (!img)
690
+ return;
691
+ img.style.display = "inline";
692
+ img.style.float = "left";
693
+ img.style.margin = "0 8px 8px 0";
694
+ scheduleImageOverlay();
695
+ handleInput();
696
+ }, title: "Float left", children: "\u27F8" }), _jsx("button", { onClick: () => {
697
+ const img = selectedImage;
698
+ if (!img)
699
+ return;
700
+ img.style.display = "inline";
701
+ img.style.float = "right";
702
+ img.style.margin = "0 0 8px 8px";
703
+ scheduleImageOverlay();
704
+ 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) => {
706
+ if (item?.url)
707
+ insertImageAtSelection(item.url);
708
+ } })), table && showTableDialog && (_jsx("div", { style: {
698
709
  position: "fixed",
699
710
  inset: 0,
700
711
  background: "rgba(0,0,0,0.35)",
@@ -737,7 +748,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
737
748
  }, children: [_jsx("button", { onClick: () => setShowTableDialog(false), children: "Cancel" }), _jsx("button", { onClick: () => {
738
749
  insertTable();
739
750
  setShowTableDialog(false);
740
- }, children: "Insert" })] })] }) })), showFormulaDialog && (_jsx("div", { style: {
751
+ }, children: "Insert" })] })] }) })), formula && showFormulaDialog && (_jsx("div", { style: {
741
752
  position: "fixed",
742
753
  inset: 0,
743
754
  background: "rgba(0,0,0,0.35)",
@@ -843,7 +854,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
843
854
  handleInput();
844
855
  }, onPaste: (e) => {
845
856
  const items = e.clipboardData?.files;
846
- if (items && items.length) {
857
+ if (media && items && items.length) {
847
858
  const hasImage = Array.from(items).some((f) => f.type.startsWith("image/"));
848
859
  if (hasImage) {
849
860
  e.preventDefault();
@@ -855,7 +866,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
855
866
  e.preventDefault();
856
867
  }
857
868
  }, onDrop: (e) => {
858
- if (e.dataTransfer?.files?.length) {
869
+ if (media && e.dataTransfer?.files?.length) {
859
870
  e.preventDefault();
860
871
  // Try to move caret to drop point
861
872
  const x = e.clientX;
@@ -904,7 +915,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
904
915
  el.innerHTML = "<p><br></p>";
905
916
  }
906
917
  }, onKeyDown: (e) => {
907
- if ((e.metaKey || e.ctrlKey) && String(e.key).toLowerCase() === "m") {
918
+ if (formula &&
919
+ (e.metaKey || e.ctrlKey) &&
920
+ String(e.key).toLowerCase() === "m") {
908
921
  e.preventDefault();
909
922
  setShowFormulaDialog(true);
910
923
  return;
@@ -924,7 +937,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
924
937
  if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
925
938
  const sel = window.getSelection();
926
939
  const cell = getClosestCell(sel?.anchorNode || null);
927
- if (cell &&
940
+ if (table &&
941
+ cell &&
928
942
  cell.parentElement &&
929
943
  cell.parentElement.parentElement) {
930
944
  const row = cell.parentElement;
@@ -0,0 +1,31 @@
1
+ export type MediaItem = {
2
+ id: string;
3
+ url: string;
4
+ width?: number;
5
+ height?: number;
6
+ sizeBytes?: number;
7
+ mimeType?: string;
8
+ hashHex?: string;
9
+ createdAt?: string;
10
+ title?: string;
11
+ alt?: string;
12
+ tags?: string[];
13
+ };
14
+ export type MediaSearchQuery = {
15
+ q?: string;
16
+ tags?: string[];
17
+ mimePrefix?: string;
18
+ hashHex?: string;
19
+ page?: number;
20
+ pageSize?: number;
21
+ };
22
+ export type MediaManagerAdapter = {
23
+ upload: (files: File[]) => Promise<MediaItem[]>;
24
+ search: (query: MediaSearchQuery) => Promise<MediaItem[]>;
25
+ };
26
+ export declare function MediaManager(props: {
27
+ open: boolean;
28
+ onClose: () => void;
29
+ adapter: MediaManagerAdapter;
30
+ onSelect: (item: MediaItem) => void;
31
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,167 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ export function MediaManager(props) {
4
+ const { open, onClose, adapter, onSelect } = props;
5
+ const [activeTab, setActiveTab] = useState("upload");
6
+ const [uploading, setUploading] = useState(false);
7
+ const [error, setError] = useState(null);
8
+ const [query, setQuery] = useState("");
9
+ const [results, setResults] = useState([]);
10
+ const fileInputRef = useRef(null);
11
+ useEffect(() => {
12
+ if (!open)
13
+ return;
14
+ setError(null);
15
+ if (activeTab === "library") {
16
+ performSearch();
17
+ }
18
+ }, [open, activeTab]);
19
+ const performSearch = async () => {
20
+ try {
21
+ const items = await adapter.search({ q: query, mimePrefix: "image/" });
22
+ setResults(items || []);
23
+ }
24
+ catch (e) {
25
+ setError("Failed to search media.");
26
+ }
27
+ };
28
+ const computeSha256Hex = async (file) => {
29
+ const buf = await file.arrayBuffer();
30
+ const digest = await crypto.subtle.digest("SHA-256", buf);
31
+ const bytes = new Uint8Array(digest);
32
+ let hex = "";
33
+ for (let i = 0; i < bytes.length; i++) {
34
+ hex += bytes[i].toString(16).padStart(2, "0");
35
+ }
36
+ return hex;
37
+ };
38
+ const handleUploadFiles = async (files) => {
39
+ if (!files || files.length === 0)
40
+ return;
41
+ const list = Array.from(files).filter((f) => f.type.startsWith("image/"));
42
+ if (list.length === 0)
43
+ return;
44
+ setUploading(true);
45
+ setError(null);
46
+ try {
47
+ // Duplicate detection by content hash (best-effort, server should also verify)
48
+ const duplicates = [];
49
+ const toUpload = [];
50
+ for (const f of list) {
51
+ try {
52
+ const hash = await computeSha256Hex(f);
53
+ const hits = await adapter.search({ hashHex: hash });
54
+ if (hits && hits.length) {
55
+ duplicates.push(hits[0]);
56
+ continue;
57
+ }
58
+ toUpload.push(f);
59
+ }
60
+ catch { }
61
+ }
62
+ if (duplicates.length) {
63
+ // Prefer duplicates immediately
64
+ onSelect(duplicates[0]);
65
+ setUploading(false);
66
+ onClose();
67
+ return;
68
+ }
69
+ if (toUpload.length) {
70
+ const uploaded = await adapter.upload(toUpload);
71
+ if (uploaded && uploaded.length) {
72
+ onSelect(uploaded[0]);
73
+ onClose();
74
+ }
75
+ }
76
+ }
77
+ catch (e) {
78
+ setError("Upload failed. Try again.");
79
+ }
80
+ finally {
81
+ setUploading(false);
82
+ }
83
+ };
84
+ if (!open)
85
+ return null;
86
+ return (_jsx("div", { style: {
87
+ position: "fixed",
88
+ inset: 0,
89
+ background: "rgba(0,0,0,0.35)",
90
+ display: "flex",
91
+ alignItems: "center",
92
+ justifyContent: "center",
93
+ zIndex: 80,
94
+ }, onClick: onClose, children: _jsxs("div", { style: {
95
+ background: "#fff",
96
+ width: 820,
97
+ maxWidth: "90vw",
98
+ maxHeight: "86vh",
99
+ borderRadius: 10,
100
+ boxShadow: "0 12px 32px rgba(0,0,0,0.22)",
101
+ display: "flex",
102
+ flexDirection: "column",
103
+ }, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { style: {
104
+ display: "flex",
105
+ alignItems: "center",
106
+ justifyContent: "space-between",
107
+ padding: "10px 14px",
108
+ borderBottom: "1px solid #eee",
109
+ }, children: [_jsxs("div", { style: { display: "flex", gap: 8 }, children: [_jsx("button", { onClick: () => setActiveTab("upload"), style: {
110
+ padding: "6px 10px",
111
+ borderRadius: 6,
112
+ border: "1px solid #ddd",
113
+ background: activeTab === "upload" ? "#f2f2f2" : "#fff",
114
+ }, children: "Upload" }), _jsx("button", { onClick: () => setActiveTab("library"), style: {
115
+ padding: "6px 10px",
116
+ borderRadius: 6,
117
+ border: "1px solid #ddd",
118
+ background: activeTab === "library" ? "#f2f2f2" : "#fff",
119
+ }, children: "Library" })] }), _jsx("button", { onClick: onClose, children: "\u2715" })] }), error && (_jsx("div", { style: { color: "#b00020", padding: "8px 14px" }, children: error })), activeTab === "upload" ? (_jsxs("div", { style: { padding: 16 }, children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: "none" }, onChange: (e) => handleUploadFiles(e.currentTarget.files) }), _jsx("div", { onClick: () => fileInputRef.current?.click(), onDragOver: (e) => {
120
+ e.preventDefault();
121
+ }, onDrop: (e) => {
122
+ e.preventDefault();
123
+ handleUploadFiles(e.dataTransfer.files);
124
+ }, style: {
125
+ border: "2px dashed #bbb",
126
+ borderRadius: 10,
127
+ padding: 24,
128
+ textAlign: "center",
129
+ color: "#333",
130
+ background: "#fafafa",
131
+ cursor: uploading ? "default" : "pointer",
132
+ opacity: uploading ? 0.7 : 1,
133
+ }, children: uploading ? "Uploading…" : "Click or drag images to upload" })] })) : (_jsxs("div", { style: {
134
+ padding: 16,
135
+ display: "flex",
136
+ flexDirection: "column",
137
+ gap: 12,
138
+ }, children: [_jsxs("div", { style: { display: "flex", gap: 8 }, children: [_jsx("input", { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search images by name, tag, etc.", style: {
139
+ flex: 1,
140
+ padding: "6px 8px",
141
+ border: "1px solid #ddd",
142
+ borderRadius: 6,
143
+ } }), _jsx("button", { onClick: performSearch, children: "Search" })] }), _jsx("div", { style: {
144
+ display: "grid",
145
+ gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))",
146
+ gap: 12,
147
+ overflowY: "auto",
148
+ paddingBottom: 16,
149
+ }, children: results.map((it) => (_jsxs("button", { onClick: () => {
150
+ onSelect(it);
151
+ onClose();
152
+ }, title: it.title || it.url, style: {
153
+ display: "block",
154
+ border: "1px solid #eee",
155
+ borderRadius: 8,
156
+ padding: 6,
157
+ background: "#fff",
158
+ textAlign: "center",
159
+ }, children: [_jsx("img", { src: it.url, alt: it.alt || "", style: {
160
+ maxWidth: "100%",
161
+ maxHeight: 100,
162
+ display: "block",
163
+ margin: "0 auto",
164
+ objectFit: "cover",
165
+ borderRadius: 6,
166
+ } }), _jsx("div", { style: { fontSize: 11, marginTop: 6, color: "#333" }, children: it.width && it.height ? `${it.width}×${it.height}` : "" })] }, it.id || it.url))) })] }))] }) }));
167
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import "katex/dist/katex.min.css";
2
2
  export { ClassicEditor } from './components/ClassicEditor';
3
+ export type { MediaManagerAdapter, MediaItem, MediaSearchQuery } from './components/MediaManager';
@@ -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 }) }));
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 }) }));
41
41
  }
42
42
  const ClassicEditorHostWithRef = React.forwardRef(ClassicEditorHost);
43
43
  function initClassicEditor(opts) {