@malaya_jeeva/rich-text-editor 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,388 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/index.js
20
+ var index_exports = {};
21
+ __export(index_exports, {
22
+ RichTextEditor: () => RichTextEditor
23
+ });
24
+ module.exports = __toCommonJS(index_exports);
25
+
26
+ // src/RichTextEditor.jsx
27
+ var import_react = require("react");
28
+ var CSS = `
29
+ * { box-sizing: border-box; }
30
+
31
+ /* \u2500\u2500 Toolbar \u2500\u2500 */
32
+ .rte-toolbar {
33
+ display: flex; flex-wrap: wrap; align-items: center; gap: 1px;
34
+ padding: 4px 8px;
35
+ background: #f8f8f8;
36
+ border-bottom: 1px solid #e0e0e0;
37
+ }
38
+ @media (prefers-color-scheme: dark) {
39
+ .rte-toolbar { background: #1e1e1e; border-color: #333; }
40
+ }
41
+
42
+ .rte-btn {
43
+ background: transparent; border: none; border-radius: 3px;
44
+ cursor: pointer; height: 30px; min-width: 30px; padding: 0 6px;
45
+ color: #444; font-size: 13px; font-weight: 500;
46
+ font-family: var(--font-sans); display: inline-flex; align-items: center;
47
+ justify-content: center; user-select: none; white-space: nowrap;
48
+ transition: background 0.1s;
49
+ flex-shrink: 0;
50
+ }
51
+ .rte-btn:hover { background: #e8e8e8; }
52
+ .rte-btn.active { background: #d0e4ff; color: #1a5fb4; }
53
+ @media (prefers-color-scheme: dark) {
54
+ .rte-btn { color: #ccc; }
55
+ .rte-btn:hover { background: #2e2e2e; }
56
+ .rte-btn.active { background: #1a3a5c; color: #90c4ff; }
57
+ }
58
+
59
+ /* thin vertical separator */
60
+ .rte-sep {
61
+ width: 1px; height: 20px; background: #d8d8d8;
62
+ margin: 0 4px; flex-shrink: 0;
63
+ }
64
+ @media (prefers-color-scheme: dark) {
65
+ .rte-sep { background: #3a3a3a; }
66
+ }
67
+
68
+ /* Block format select */
69
+ .rte-select {
70
+ height: 28px; border: 1px solid #d8d8d8; border-radius: 3px;
71
+ background: #fff; color: #333;
72
+ font-size: 12px; padding: 0 22px 0 7px; cursor: pointer;
73
+ outline: none; font-family: var(--font-sans);
74
+ appearance: none; -webkit-appearance: none;
75
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E");
76
+ background-repeat: no-repeat; background-position: right 6px center;
77
+ }
78
+ @media (prefers-color-scheme: dark) {
79
+ .rte-select { background-color: #2a2a2a; border-color: #444; color: #ccc; }
80
+ }
81
+
82
+ /* color swatch under A */
83
+ .rte-swatch { width: 14px; height: 3px; border-radius: 1px; margin-top: 2px; }
84
+
85
+ /* \u2500\u2500 Editor body \u2500\u2500 */
86
+ .rte-wrap {
87
+ background: #fff;
88
+ }
89
+ @media (prefers-color-scheme: dark) {
90
+ .rte-wrap { background: #141414; }
91
+ }
92
+ .rte-body {
93
+ min-height: 280px; outline: none; font-size: 15px; line-height: 1.75;
94
+ color: #222; caret-color: #222; padding: 18px 20px;
95
+ }
96
+ @media (prefers-color-scheme: dark) {
97
+ .rte-body { color: #ddd; caret-color: #ddd; }
98
+ }
99
+ .rte-body h1 { font-size: 26px; font-weight: 600; margin: 0.6em 0 0.2em; }
100
+ .rte-body h2 { font-size: 20px; font-weight: 600; margin: 0.6em 0 0.2em; }
101
+ .rte-body h3 { font-size: 16px; font-weight: 600; margin: 0.6em 0 0.2em; }
102
+ .rte-body p { margin: 0.2em 0; }
103
+ .rte-body ol { list-style-type: decimal; padding-left: 1.6em; margin: 0.3em 0; }
104
+ .rte-body ol ol { list-style-type: lower-alpha; padding-left: 1.6em; }
105
+ .rte-body ol ol ol { list-style-type: lower-roman; padding-left: 1.6em; }
106
+ .rte-body ol ol ol ol { list-style-type: decimal; padding-left: 1.6em; }
107
+ .rte-body ul { list-style-type: disc; padding-left: 1.6em; margin: 0.3em 0; }
108
+ .rte-body ul ul { list-style-type: circle; padding-left: 1.6em; }
109
+ .rte-body ul ul ul { list-style-type: square; padding-left: 1.6em; }
110
+ .rte-body li { margin: 0.1em 0; }
111
+ .rte-body a { color: #1a6fc4; text-decoration: underline; cursor: pointer; }
112
+ .rte-body pre { background: #f4f4f4; border: 1px solid #e0e0e0; border-radius: 4px; padding: 12px 14px; margin: 0.6em 0; overflow-x: auto; }
113
+ .rte-body code { font-family: var(--font-mono); font-size: 13px; }
114
+ .rte-body:empty:before { content: attr(data-ph); color: #aaa; pointer-events: none; }
115
+
116
+ .rte-code {
117
+ min-height: 280px; width: 100%; background: #1e1e2e;
118
+ color: #cdd6f4; font-family: var(--font-mono); font-size: 13px;
119
+ line-height: 1.6; padding: 18px 20px; border: none; outline: none;
120
+ resize: vertical;
121
+ }
122
+ .rte-footer {
123
+ font-size: 11px; color: #999; padding: 4px 20px 5px;
124
+ border-top: 1px solid #e8e8e8; background: #fafafa;
125
+ }
126
+ @media (prefers-color-scheme: dark) {
127
+ .rte-footer { border-color: #2a2a2a; background: #1a1a1a; color: #555; }
128
+ }
129
+ `;
130
+ var Btn = ({ onClick, title, active, children, style }) => /* @__PURE__ */ React.createElement(
131
+ "button",
132
+ {
133
+ className: `rte-btn${active ? " active" : ""}`,
134
+ style,
135
+ onMouseDown: (e) => {
136
+ e.preventDefault();
137
+ onClick(e);
138
+ },
139
+ title
140
+ },
141
+ children
142
+ );
143
+ var Sep = () => /* @__PURE__ */ React.createElement("div", { className: "rte-sep" });
144
+ var IcoUL = () => /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, /* @__PURE__ */ React.createElement("circle", { cx: "2", cy: "4", r: "1.3", fill: "currentColor" }), /* @__PURE__ */ React.createElement("circle", { cx: "2", cy: "8", r: "1.3", fill: "currentColor" }), /* @__PURE__ */ React.createElement("circle", { cx: "2", cy: "12", r: "1.3", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "5.5", y: "3.2", width: "8.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "5.5", y: "7.2", width: "8.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "5.5", y: "11.2", width: "8.5", height: "1.6", rx: ".8", fill: "currentColor" }));
145
+ var IcoOL = () => /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, /* @__PURE__ */ React.createElement("text", { x: "0", y: "5", fontSize: "5.5", fill: "currentColor", fontFamily: "monospace" }, "1."), /* @__PURE__ */ React.createElement("text", { x: "0", y: "9.5", fontSize: "5.5", fill: "currentColor", fontFamily: "monospace" }, "2."), /* @__PURE__ */ React.createElement("text", { x: "0", y: "14", fontSize: "5.5", fill: "currentColor", fontFamily: "monospace" }, "3."), /* @__PURE__ */ React.createElement("rect", { x: "5.5", y: "3.2", width: "8.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "5.5", y: "7.2", width: "8.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "5.5", y: "11.2", width: "8.5", height: "1.6", rx: ".8", fill: "currentColor" }));
146
+ var IcoIndent = () => /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, /* @__PURE__ */ React.createElement("rect", { x: "1", y: "2", width: "13", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "4.5", y: "6", width: "9.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "4.5", y: "10", width: "9.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("path", { d: "M1 6.5L3.5 8.25L1 10V6.5Z", fill: "currentColor" }));
147
+ var IcoOutdent = () => /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, /* @__PURE__ */ React.createElement("rect", { x: "1", y: "2", width: "13", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "4.5", y: "6", width: "9.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "4.5", y: "10", width: "9.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("path", { d: "M3.5 6.5L1 8.25L3.5 10V6.5Z", fill: "currentColor" }));
148
+ var IcoLink = () => /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, /* @__PURE__ */ React.createElement("path", { d: "M6 9C6.4 9.6 7.1 10 7.8 10H9.8C10.6 10 11.4 9.6 11.9 9C12.4 8.4 12.6 7.6 12.6 6.8C12.6 6 12.4 5.3 11.9 4.7C11.4 4.1 10.6 3.8 9.8 3.8H8.6", stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round" }), /* @__PURE__ */ React.createElement("path", { d: "M9 6C8.6 5.4 7.9 5 7.2 5H5.2C4.4 5 3.6 5.4 3.1 6C2.6 6.6 2.4 7.4 2.4 8.2C2.4 9 2.6 9.7 3.1 10.3C3.6 10.9 4.4 11.2 5.2 11.2H6.4", stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round" }));
149
+ var IcoCode = () => /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, /* @__PURE__ */ React.createElement("path", { d: "M5 4.5L1.5 7.5L5 10.5", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }), /* @__PURE__ */ React.createElement("path", { d: "M10 4.5L13.5 7.5L10 10.5", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }), /* @__PURE__ */ React.createElement("path", { d: "M8.5 2L6.5 13", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round" }));
150
+ var IcoCopy = () => /* @__PURE__ */ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none" }, /* @__PURE__ */ React.createElement("rect", { x: "1", y: "3", width: "8.5", height: "10", rx: "1.5", stroke: "currentColor", strokeWidth: "1.2" }), /* @__PURE__ */ React.createElement("path", { d: "M4 1H12.5C13.1 1 13.5 1.4 13.5 2V10", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round" }));
151
+ var AlignIco = ({ t }) => {
152
+ const w = { left: [13, 9, 11], center: [9, 7, 11], right: [13, 9, 11], justify: [13, 13, 13] }[t];
153
+ const x = { left: [1, 1, 1], center: [3, 4, 2], right: [1, 1, 1], justify: [1, 1, 1] }[t];
154
+ const xr = { left: [0, 0, 0], center: [0, 0, 0], right: [1, 1, 1], justify: [0, 0, 0] }[t];
155
+ return /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, [0, 1, 2].map((i) => /* @__PURE__ */ React.createElement("rect", { key: i, x: t === "right" ? 15 - w[i] - 1 : x[i], y: [2, 6.5, 11][i], width: w[i], height: "1.6", rx: ".8", fill: "currentColor" })));
156
+ };
157
+ function RichTextEditor() {
158
+ const editorRef = (0, import_react.useRef)(null);
159
+ const colorRef = (0, import_react.useRef)(null);
160
+ const [isCode, setIsCode] = (0, import_react.useState)(false);
161
+ const [codeVal, setCodeVal] = (0, import_react.useState)("");
162
+ const [fmt, setFmt] = (0, import_react.useState)({ block: "p" });
163
+ const [color, setColor] = (0, import_react.useState)("#e74c3c");
164
+ const [words, setWords] = (0, import_react.useState)(0);
165
+ const [linkBar, setLinkBar] = (0, import_react.useState)(false);
166
+ const [linkUrl, setLinkUrl] = (0, import_react.useState)("https://");
167
+ const savedRangeRef = (0, import_react.useRef)(null);
168
+ const exec = (0, import_react.useCallback)((cmd, val = null) => {
169
+ var _a;
170
+ (_a = editorRef.current) == null ? void 0 : _a.focus();
171
+ document.execCommand(cmd, false, val);
172
+ refresh();
173
+ }, []);
174
+ const refresh = (0, import_react.useCallback)(() => {
175
+ var _a;
176
+ const raw = document.queryCommandValue("formatBlock").toLowerCase().replace(/[<>]/g, "");
177
+ const block = ["h1", "h2", "h3"].includes(raw) ? raw : "p";
178
+ const c = document.queryCommandValue("foreColor");
179
+ if (c && c !== "false") {
180
+ const m = c.match(/\d+/g);
181
+ if (m) setColor("#" + m.slice(0, 3).map((n) => (+n).toString(16).padStart(2, "0")).join(""));
182
+ }
183
+ setFmt({
184
+ bold: document.queryCommandState("bold"),
185
+ italic: document.queryCommandState("italic"),
186
+ underline: document.queryCommandState("underline"),
187
+ ul: document.queryCommandState("insertUnorderedList"),
188
+ ol: document.queryCommandState("insertOrderedList"),
189
+ aL: document.queryCommandState("justifyLeft"),
190
+ aC: document.queryCommandState("justifyCenter"),
191
+ aR: document.queryCommandState("justifyRight"),
192
+ aJ: document.queryCommandState("justifyFull"),
193
+ block
194
+ });
195
+ const txt = ((_a = editorRef.current) == null ? void 0 : _a.innerText) || "";
196
+ setWords(txt.trim() ? txt.trim().split(/\s+/).length : 0);
197
+ }, []);
198
+ const handleKeyDown = (0, import_react.useCallback)((e) => {
199
+ var _a, _b;
200
+ if (e.key === "Tab") {
201
+ e.preventDefault();
202
+ const node = (_a = window.getSelection()) == null ? void 0 : _a.anchorNode;
203
+ const li = (node == null ? void 0 : node.nodeType) === 1 ? node.closest("li") : (_b = node == null ? void 0 : node.parentElement) == null ? void 0 : _b.closest("li");
204
+ if (li) exec(e.shiftKey ? "outdent" : "indent");
205
+ else document.execCommand("insertText", false, "\xA0\xA0\xA0\xA0");
206
+ }
207
+ if (e.ctrlKey || e.metaKey) {
208
+ if (e.key === "b") {
209
+ e.preventDefault();
210
+ exec("bold");
211
+ }
212
+ if (e.key === "i") {
213
+ e.preventDefault();
214
+ exec("italic");
215
+ }
216
+ if (e.key === "u") {
217
+ e.preventDefault();
218
+ exec("underline");
219
+ }
220
+ }
221
+ }, [exec]);
222
+ const toCode = () => {
223
+ var _a;
224
+ setCodeVal(((_a = editorRef.current) == null ? void 0 : _a.innerHTML) || "");
225
+ setIsCode(true);
226
+ };
227
+ const toVisual = () => {
228
+ if (editorRef.current) editorRef.current.innerHTML = codeVal;
229
+ setIsCode(false);
230
+ refresh();
231
+ };
232
+ const openLinkBar = () => {
233
+ const sel = window.getSelection();
234
+ savedRangeRef.current = (sel == null ? void 0 : sel.rangeCount) ? sel.getRangeAt(0).cloneRange() : null;
235
+ setLinkUrl((sel == null ? void 0 : sel.toString()) ? "https://" : "https://");
236
+ setLinkBar(true);
237
+ };
238
+ const applyLink = () => {
239
+ var _a, _b, _c;
240
+ if (!linkUrl) return;
241
+ (_a = editorRef.current) == null ? void 0 : _a.focus();
242
+ const sel = window.getSelection();
243
+ if (savedRangeRef.current) {
244
+ sel.removeAllRanges();
245
+ sel.addRange(savedRangeRef.current);
246
+ }
247
+ if ((_b = savedRangeRef.current) == null ? void 0 : _b.toString()) {
248
+ document.execCommand("createLink", false, linkUrl);
249
+ } else {
250
+ const a = document.createElement("a");
251
+ a.href = linkUrl;
252
+ a.textContent = linkUrl;
253
+ (_c = savedRangeRef.current) == null ? void 0 : _c.insertNode(a);
254
+ }
255
+ setLinkBar(false);
256
+ setLinkUrl("https://");
257
+ refresh();
258
+ };
259
+ const insertCodeBlock = () => {
260
+ const sel = window.getSelection();
261
+ if (!(sel == null ? void 0 : sel.rangeCount)) return;
262
+ const range = sel.getRangeAt(0);
263
+ const pre = document.createElement("pre");
264
+ const code = document.createElement("code");
265
+ code.textContent = range.toString() || "// code here";
266
+ pre.appendChild(code);
267
+ range.deleteContents();
268
+ range.insertNode(pre);
269
+ const br = document.createElement("br");
270
+ pre.insertAdjacentElement("afterend", br);
271
+ range.setStartAfter(br);
272
+ range.collapse(true);
273
+ sel.removeAllRanges();
274
+ sel.addRange(range);
275
+ refresh();
276
+ };
277
+ const copyHtml = () => {
278
+ var _a;
279
+ return navigator.clipboard.writeText(isCode ? codeVal : ((_a = editorRef.current) == null ? void 0 : _a.innerHTML) || "").catch(() => {
280
+ });
281
+ };
282
+ (0, import_react.useEffect)(() => {
283
+ if (editorRef.current) {
284
+ editorRef.current.innerHTML = "<p>Start writing here...</p>";
285
+ refresh();
286
+ }
287
+ }, []);
288
+ return /* @__PURE__ */ React.createElement("div", { style: { padding: "1rem 0" } }, /* @__PURE__ */ React.createElement("style", null, CSS), /* @__PURE__ */ React.createElement("div", { style: { border: "1px solid #d8d8d8", borderRadius: 4, overflow: "hidden", boxShadow: "0 1px 3px rgba(0,0,0,0.06)" } }, /* @__PURE__ */ React.createElement("div", { className: "rte-toolbar" }, !isCode && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("bold"), title: "Bold (Ctrl+B)", active: fmt.bold, style: { fontWeight: 800, fontFamily: "Georgia,serif" } }, "B"), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("italic"), title: "Italic (Ctrl+I)", active: fmt.italic, style: { fontStyle: "italic", fontFamily: "Georgia,serif" } }, "I"), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("underline"), title: "Underline (Ctrl+U)", active: fmt.underline, style: { textDecoration: "underline" } }, "U"), /* @__PURE__ */ React.createElement(Btn, { onClick: () => {
289
+ var _a;
290
+ return (_a = colorRef.current) == null ? void 0 : _a.click();
291
+ }, title: "Text color", style: { flexDirection: "column", gap: 1, padding: "3px 6px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontSize: 13, fontWeight: 800, fontFamily: "Georgia,serif", lineHeight: 1, color: "#222" } }, "A"), /* @__PURE__ */ React.createElement("span", { className: "rte-swatch", style: { background: color } })), /* @__PURE__ */ React.createElement(
292
+ "input",
293
+ {
294
+ ref: colorRef,
295
+ type: "color",
296
+ value: color,
297
+ onChange: (e) => {
298
+ setColor(e.target.value);
299
+ exec("foreColor", e.target.value);
300
+ },
301
+ style: { position: "absolute", opacity: 0, width: 0, height: 0, pointerEvents: "none" }
302
+ }
303
+ ), /* @__PURE__ */ React.createElement(Sep, null), /* @__PURE__ */ React.createElement(
304
+ "select",
305
+ {
306
+ className: "rte-select",
307
+ value: fmt.block || "p",
308
+ onMouseDown: () => {
309
+ var _a;
310
+ return (_a = editorRef.current) == null ? void 0 : _a.focus();
311
+ },
312
+ onChange: (e) => {
313
+ var _a;
314
+ (_a = editorRef.current) == null ? void 0 : _a.focus();
315
+ document.execCommand("formatBlock", false, e.target.value);
316
+ refresh();
317
+ }
318
+ },
319
+ /* @__PURE__ */ React.createElement("option", { value: "p" }, "Paragraph"),
320
+ /* @__PURE__ */ React.createElement("option", { value: "h1" }, "Heading 1"),
321
+ /* @__PURE__ */ React.createElement("option", { value: "h2" }, "Heading 2"),
322
+ /* @__PURE__ */ React.createElement("option", { value: "h3" }, "Heading 3")
323
+ ), /* @__PURE__ */ React.createElement(Sep, null), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("justifyLeft"), title: "Align left", active: fmt.aL }, /* @__PURE__ */ React.createElement(AlignIco, { t: "left" })), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("justifyCenter"), title: "Align center", active: fmt.aC }, /* @__PURE__ */ React.createElement(AlignIco, { t: "center" })), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("justifyRight"), title: "Align right", active: fmt.aR }, /* @__PURE__ */ React.createElement(AlignIco, { t: "right" })), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("justifyFull"), title: "Justify", active: fmt.aJ }, /* @__PURE__ */ React.createElement(AlignIco, { t: "justify" })), /* @__PURE__ */ React.createElement(Sep, null), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("insertUnorderedList"), title: "Bullet list", active: fmt.ul }, /* @__PURE__ */ React.createElement(IcoUL, null)), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("insertOrderedList"), title: "Numbered list", active: fmt.ol }, /* @__PURE__ */ React.createElement(IcoOL, null)), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("indent"), title: "Indent (Tab)" }, /* @__PURE__ */ React.createElement(IcoIndent, null)), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("outdent"), title: "Outdent (Shift+Tab)" }, /* @__PURE__ */ React.createElement(IcoOutdent, null)), /* @__PURE__ */ React.createElement(Sep, null), /* @__PURE__ */ React.createElement(Btn, { onClick: openLinkBar, title: "Insert link", active: linkBar }, /* @__PURE__ */ React.createElement(IcoLink, null)), /* @__PURE__ */ React.createElement(Btn, { onClick: insertCodeBlock, title: "Code block" }, /* @__PURE__ */ React.createElement(IcoCode, null)), /* @__PURE__ */ React.createElement(Sep, null)), /* @__PURE__ */ React.createElement(
324
+ Btn,
325
+ {
326
+ onClick: isCode ? toVisual : toCode,
327
+ title: "Toggle HTML source",
328
+ active: isCode,
329
+ style: { fontSize: 12, fontWeight: 600, letterSpacing: "0.03em", padding: "0 8px" }
330
+ },
331
+ isCode ? "Visual" : "HTML"
332
+ ), /* @__PURE__ */ React.createElement(Btn, { onClick: copyHtml, title: "Copy HTML" }, /* @__PURE__ */ React.createElement(IcoCopy, null)), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12, color: "#aaa" } }, words, " ", words === 1 ? "word" : "words")), linkBar && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6, padding: "6px 10px", background: "#f0f4ff", borderBottom: "1px solid #c8d8f8" } }, /* @__PURE__ */ React.createElement(IcoLink, null), /* @__PURE__ */ React.createElement(
333
+ "input",
334
+ {
335
+ autoFocus: true,
336
+ type: "text",
337
+ value: linkUrl,
338
+ onChange: (e) => setLinkUrl(e.target.value),
339
+ onKeyDown: (e) => {
340
+ if (e.key === "Enter") applyLink();
341
+ if (e.key === "Escape") setLinkBar(false);
342
+ },
343
+ placeholder: "https://example.com",
344
+ style: { flex: 1, height: 26, border: "1px solid #b0c4f0", borderRadius: 3, padding: "0 8px", fontSize: 13, outline: "none", fontFamily: "var(--font-sans)" }
345
+ }
346
+ ), /* @__PURE__ */ React.createElement(
347
+ "button",
348
+ {
349
+ onClick: applyLink,
350
+ style: { height: 26, padding: "0 12px", background: "#1a6fc4", color: "#fff", border: "none", borderRadius: 3, fontSize: 12, fontWeight: 600, cursor: "pointer" }
351
+ },
352
+ "Apply"
353
+ ), /* @__PURE__ */ React.createElement(
354
+ "button",
355
+ {
356
+ onClick: () => setLinkBar(false),
357
+ style: { height: 26, padding: "0 10px", background: "transparent", color: "#666", border: "1px solid #ccc", borderRadius: 3, fontSize: 12, cursor: "pointer" }
358
+ },
359
+ "Cancel"
360
+ )), /* @__PURE__ */ React.createElement("div", { className: "rte-wrap" }, /* @__PURE__ */ React.createElement(
361
+ "textarea",
362
+ {
363
+ className: "rte-code",
364
+ value: codeVal,
365
+ onChange: (e) => setCodeVal(e.target.value),
366
+ spellCheck: false,
367
+ style: { display: isCode ? "block" : "none" }
368
+ }
369
+ ), /* @__PURE__ */ React.createElement(
370
+ "div",
371
+ {
372
+ ref: editorRef,
373
+ className: "rte-body",
374
+ contentEditable: true,
375
+ suppressContentEditableWarning: true,
376
+ "data-ph": "Start typing...",
377
+ onKeyDown: handleKeyDown,
378
+ onKeyUp: refresh,
379
+ onMouseUp: refresh,
380
+ onSelect: refresh,
381
+ style: { display: isCode ? "none" : "block" }
382
+ }
383
+ )), !isCode && /* @__PURE__ */ React.createElement("div", { className: "rte-footer" }, "Inside a list: ", /* @__PURE__ */ React.createElement("strong", null, "Tab"), " to go deeper \xA0\xB7\xA0 ", /* @__PURE__ */ React.createElement("strong", null, "Shift+Tab"), " to go back up")));
384
+ }
385
+ // Annotate the CommonJS export names for ESM import in node:
386
+ 0 && (module.exports = {
387
+ RichTextEditor
388
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,362 @@
1
+ // src/RichTextEditor.jsx
2
+ import { useState, useRef, useCallback, useEffect } from "react";
3
+ var CSS = `
4
+ * { box-sizing: border-box; }
5
+
6
+ /* \u2500\u2500 Toolbar \u2500\u2500 */
7
+ .rte-toolbar {
8
+ display: flex; flex-wrap: wrap; align-items: center; gap: 1px;
9
+ padding: 4px 8px;
10
+ background: #f8f8f8;
11
+ border-bottom: 1px solid #e0e0e0;
12
+ }
13
+ @media (prefers-color-scheme: dark) {
14
+ .rte-toolbar { background: #1e1e1e; border-color: #333; }
15
+ }
16
+
17
+ .rte-btn {
18
+ background: transparent; border: none; border-radius: 3px;
19
+ cursor: pointer; height: 30px; min-width: 30px; padding: 0 6px;
20
+ color: #444; font-size: 13px; font-weight: 500;
21
+ font-family: var(--font-sans); display: inline-flex; align-items: center;
22
+ justify-content: center; user-select: none; white-space: nowrap;
23
+ transition: background 0.1s;
24
+ flex-shrink: 0;
25
+ }
26
+ .rte-btn:hover { background: #e8e8e8; }
27
+ .rte-btn.active { background: #d0e4ff; color: #1a5fb4; }
28
+ @media (prefers-color-scheme: dark) {
29
+ .rte-btn { color: #ccc; }
30
+ .rte-btn:hover { background: #2e2e2e; }
31
+ .rte-btn.active { background: #1a3a5c; color: #90c4ff; }
32
+ }
33
+
34
+ /* thin vertical separator */
35
+ .rte-sep {
36
+ width: 1px; height: 20px; background: #d8d8d8;
37
+ margin: 0 4px; flex-shrink: 0;
38
+ }
39
+ @media (prefers-color-scheme: dark) {
40
+ .rte-sep { background: #3a3a3a; }
41
+ }
42
+
43
+ /* Block format select */
44
+ .rte-select {
45
+ height: 28px; border: 1px solid #d8d8d8; border-radius: 3px;
46
+ background: #fff; color: #333;
47
+ font-size: 12px; padding: 0 22px 0 7px; cursor: pointer;
48
+ outline: none; font-family: var(--font-sans);
49
+ appearance: none; -webkit-appearance: none;
50
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E");
51
+ background-repeat: no-repeat; background-position: right 6px center;
52
+ }
53
+ @media (prefers-color-scheme: dark) {
54
+ .rte-select { background-color: #2a2a2a; border-color: #444; color: #ccc; }
55
+ }
56
+
57
+ /* color swatch under A */
58
+ .rte-swatch { width: 14px; height: 3px; border-radius: 1px; margin-top: 2px; }
59
+
60
+ /* \u2500\u2500 Editor body \u2500\u2500 */
61
+ .rte-wrap {
62
+ background: #fff;
63
+ }
64
+ @media (prefers-color-scheme: dark) {
65
+ .rte-wrap { background: #141414; }
66
+ }
67
+ .rte-body {
68
+ min-height: 280px; outline: none; font-size: 15px; line-height: 1.75;
69
+ color: #222; caret-color: #222; padding: 18px 20px;
70
+ }
71
+ @media (prefers-color-scheme: dark) {
72
+ .rte-body { color: #ddd; caret-color: #ddd; }
73
+ }
74
+ .rte-body h1 { font-size: 26px; font-weight: 600; margin: 0.6em 0 0.2em; }
75
+ .rte-body h2 { font-size: 20px; font-weight: 600; margin: 0.6em 0 0.2em; }
76
+ .rte-body h3 { font-size: 16px; font-weight: 600; margin: 0.6em 0 0.2em; }
77
+ .rte-body p { margin: 0.2em 0; }
78
+ .rte-body ol { list-style-type: decimal; padding-left: 1.6em; margin: 0.3em 0; }
79
+ .rte-body ol ol { list-style-type: lower-alpha; padding-left: 1.6em; }
80
+ .rte-body ol ol ol { list-style-type: lower-roman; padding-left: 1.6em; }
81
+ .rte-body ol ol ol ol { list-style-type: decimal; padding-left: 1.6em; }
82
+ .rte-body ul { list-style-type: disc; padding-left: 1.6em; margin: 0.3em 0; }
83
+ .rte-body ul ul { list-style-type: circle; padding-left: 1.6em; }
84
+ .rte-body ul ul ul { list-style-type: square; padding-left: 1.6em; }
85
+ .rte-body li { margin: 0.1em 0; }
86
+ .rte-body a { color: #1a6fc4; text-decoration: underline; cursor: pointer; }
87
+ .rte-body pre { background: #f4f4f4; border: 1px solid #e0e0e0; border-radius: 4px; padding: 12px 14px; margin: 0.6em 0; overflow-x: auto; }
88
+ .rte-body code { font-family: var(--font-mono); font-size: 13px; }
89
+ .rte-body:empty:before { content: attr(data-ph); color: #aaa; pointer-events: none; }
90
+
91
+ .rte-code {
92
+ min-height: 280px; width: 100%; background: #1e1e2e;
93
+ color: #cdd6f4; font-family: var(--font-mono); font-size: 13px;
94
+ line-height: 1.6; padding: 18px 20px; border: none; outline: none;
95
+ resize: vertical;
96
+ }
97
+ .rte-footer {
98
+ font-size: 11px; color: #999; padding: 4px 20px 5px;
99
+ border-top: 1px solid #e8e8e8; background: #fafafa;
100
+ }
101
+ @media (prefers-color-scheme: dark) {
102
+ .rte-footer { border-color: #2a2a2a; background: #1a1a1a; color: #555; }
103
+ }
104
+ `;
105
+ var Btn = ({ onClick, title, active, children, style }) => /* @__PURE__ */ React.createElement(
106
+ "button",
107
+ {
108
+ className: `rte-btn${active ? " active" : ""}`,
109
+ style,
110
+ onMouseDown: (e) => {
111
+ e.preventDefault();
112
+ onClick(e);
113
+ },
114
+ title
115
+ },
116
+ children
117
+ );
118
+ var Sep = () => /* @__PURE__ */ React.createElement("div", { className: "rte-sep" });
119
+ var IcoUL = () => /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, /* @__PURE__ */ React.createElement("circle", { cx: "2", cy: "4", r: "1.3", fill: "currentColor" }), /* @__PURE__ */ React.createElement("circle", { cx: "2", cy: "8", r: "1.3", fill: "currentColor" }), /* @__PURE__ */ React.createElement("circle", { cx: "2", cy: "12", r: "1.3", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "5.5", y: "3.2", width: "8.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "5.5", y: "7.2", width: "8.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "5.5", y: "11.2", width: "8.5", height: "1.6", rx: ".8", fill: "currentColor" }));
120
+ var IcoOL = () => /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, /* @__PURE__ */ React.createElement("text", { x: "0", y: "5", fontSize: "5.5", fill: "currentColor", fontFamily: "monospace" }, "1."), /* @__PURE__ */ React.createElement("text", { x: "0", y: "9.5", fontSize: "5.5", fill: "currentColor", fontFamily: "monospace" }, "2."), /* @__PURE__ */ React.createElement("text", { x: "0", y: "14", fontSize: "5.5", fill: "currentColor", fontFamily: "monospace" }, "3."), /* @__PURE__ */ React.createElement("rect", { x: "5.5", y: "3.2", width: "8.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "5.5", y: "7.2", width: "8.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "5.5", y: "11.2", width: "8.5", height: "1.6", rx: ".8", fill: "currentColor" }));
121
+ var IcoIndent = () => /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, /* @__PURE__ */ React.createElement("rect", { x: "1", y: "2", width: "13", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "4.5", y: "6", width: "9.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "4.5", y: "10", width: "9.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("path", { d: "M1 6.5L3.5 8.25L1 10V6.5Z", fill: "currentColor" }));
122
+ var IcoOutdent = () => /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, /* @__PURE__ */ React.createElement("rect", { x: "1", y: "2", width: "13", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "4.5", y: "6", width: "9.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("rect", { x: "4.5", y: "10", width: "9.5", height: "1.6", rx: ".8", fill: "currentColor" }), /* @__PURE__ */ React.createElement("path", { d: "M3.5 6.5L1 8.25L3.5 10V6.5Z", fill: "currentColor" }));
123
+ var IcoLink = () => /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, /* @__PURE__ */ React.createElement("path", { d: "M6 9C6.4 9.6 7.1 10 7.8 10H9.8C10.6 10 11.4 9.6 11.9 9C12.4 8.4 12.6 7.6 12.6 6.8C12.6 6 12.4 5.3 11.9 4.7C11.4 4.1 10.6 3.8 9.8 3.8H8.6", stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round" }), /* @__PURE__ */ React.createElement("path", { d: "M9 6C8.6 5.4 7.9 5 7.2 5H5.2C4.4 5 3.6 5.4 3.1 6C2.6 6.6 2.4 7.4 2.4 8.2C2.4 9 2.6 9.7 3.1 10.3C3.6 10.9 4.4 11.2 5.2 11.2H6.4", stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round" }));
124
+ var IcoCode = () => /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, /* @__PURE__ */ React.createElement("path", { d: "M5 4.5L1.5 7.5L5 10.5", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }), /* @__PURE__ */ React.createElement("path", { d: "M10 4.5L13.5 7.5L10 10.5", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }), /* @__PURE__ */ React.createElement("path", { d: "M8.5 2L6.5 13", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round" }));
125
+ var IcoCopy = () => /* @__PURE__ */ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none" }, /* @__PURE__ */ React.createElement("rect", { x: "1", y: "3", width: "8.5", height: "10", rx: "1.5", stroke: "currentColor", strokeWidth: "1.2" }), /* @__PURE__ */ React.createElement("path", { d: "M4 1H12.5C13.1 1 13.5 1.4 13.5 2V10", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round" }));
126
+ var AlignIco = ({ t }) => {
127
+ const w = { left: [13, 9, 11], center: [9, 7, 11], right: [13, 9, 11], justify: [13, 13, 13] }[t];
128
+ const x = { left: [1, 1, 1], center: [3, 4, 2], right: [1, 1, 1], justify: [1, 1, 1] }[t];
129
+ const xr = { left: [0, 0, 0], center: [0, 0, 0], right: [1, 1, 1], justify: [0, 0, 0] }[t];
130
+ return /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none" }, [0, 1, 2].map((i) => /* @__PURE__ */ React.createElement("rect", { key: i, x: t === "right" ? 15 - w[i] - 1 : x[i], y: [2, 6.5, 11][i], width: w[i], height: "1.6", rx: ".8", fill: "currentColor" })));
131
+ };
132
+ function RichTextEditor() {
133
+ const editorRef = useRef(null);
134
+ const colorRef = useRef(null);
135
+ const [isCode, setIsCode] = useState(false);
136
+ const [codeVal, setCodeVal] = useState("");
137
+ const [fmt, setFmt] = useState({ block: "p" });
138
+ const [color, setColor] = useState("#e74c3c");
139
+ const [words, setWords] = useState(0);
140
+ const [linkBar, setLinkBar] = useState(false);
141
+ const [linkUrl, setLinkUrl] = useState("https://");
142
+ const savedRangeRef = useRef(null);
143
+ const exec = useCallback((cmd, val = null) => {
144
+ var _a;
145
+ (_a = editorRef.current) == null ? void 0 : _a.focus();
146
+ document.execCommand(cmd, false, val);
147
+ refresh();
148
+ }, []);
149
+ const refresh = useCallback(() => {
150
+ var _a;
151
+ const raw = document.queryCommandValue("formatBlock").toLowerCase().replace(/[<>]/g, "");
152
+ const block = ["h1", "h2", "h3"].includes(raw) ? raw : "p";
153
+ const c = document.queryCommandValue("foreColor");
154
+ if (c && c !== "false") {
155
+ const m = c.match(/\d+/g);
156
+ if (m) setColor("#" + m.slice(0, 3).map((n) => (+n).toString(16).padStart(2, "0")).join(""));
157
+ }
158
+ setFmt({
159
+ bold: document.queryCommandState("bold"),
160
+ italic: document.queryCommandState("italic"),
161
+ underline: document.queryCommandState("underline"),
162
+ ul: document.queryCommandState("insertUnorderedList"),
163
+ ol: document.queryCommandState("insertOrderedList"),
164
+ aL: document.queryCommandState("justifyLeft"),
165
+ aC: document.queryCommandState("justifyCenter"),
166
+ aR: document.queryCommandState("justifyRight"),
167
+ aJ: document.queryCommandState("justifyFull"),
168
+ block
169
+ });
170
+ const txt = ((_a = editorRef.current) == null ? void 0 : _a.innerText) || "";
171
+ setWords(txt.trim() ? txt.trim().split(/\s+/).length : 0);
172
+ }, []);
173
+ const handleKeyDown = useCallback((e) => {
174
+ var _a, _b;
175
+ if (e.key === "Tab") {
176
+ e.preventDefault();
177
+ const node = (_a = window.getSelection()) == null ? void 0 : _a.anchorNode;
178
+ const li = (node == null ? void 0 : node.nodeType) === 1 ? node.closest("li") : (_b = node == null ? void 0 : node.parentElement) == null ? void 0 : _b.closest("li");
179
+ if (li) exec(e.shiftKey ? "outdent" : "indent");
180
+ else document.execCommand("insertText", false, "\xA0\xA0\xA0\xA0");
181
+ }
182
+ if (e.ctrlKey || e.metaKey) {
183
+ if (e.key === "b") {
184
+ e.preventDefault();
185
+ exec("bold");
186
+ }
187
+ if (e.key === "i") {
188
+ e.preventDefault();
189
+ exec("italic");
190
+ }
191
+ if (e.key === "u") {
192
+ e.preventDefault();
193
+ exec("underline");
194
+ }
195
+ }
196
+ }, [exec]);
197
+ const toCode = () => {
198
+ var _a;
199
+ setCodeVal(((_a = editorRef.current) == null ? void 0 : _a.innerHTML) || "");
200
+ setIsCode(true);
201
+ };
202
+ const toVisual = () => {
203
+ if (editorRef.current) editorRef.current.innerHTML = codeVal;
204
+ setIsCode(false);
205
+ refresh();
206
+ };
207
+ const openLinkBar = () => {
208
+ const sel = window.getSelection();
209
+ savedRangeRef.current = (sel == null ? void 0 : sel.rangeCount) ? sel.getRangeAt(0).cloneRange() : null;
210
+ setLinkUrl((sel == null ? void 0 : sel.toString()) ? "https://" : "https://");
211
+ setLinkBar(true);
212
+ };
213
+ const applyLink = () => {
214
+ var _a, _b, _c;
215
+ if (!linkUrl) return;
216
+ (_a = editorRef.current) == null ? void 0 : _a.focus();
217
+ const sel = window.getSelection();
218
+ if (savedRangeRef.current) {
219
+ sel.removeAllRanges();
220
+ sel.addRange(savedRangeRef.current);
221
+ }
222
+ if ((_b = savedRangeRef.current) == null ? void 0 : _b.toString()) {
223
+ document.execCommand("createLink", false, linkUrl);
224
+ } else {
225
+ const a = document.createElement("a");
226
+ a.href = linkUrl;
227
+ a.textContent = linkUrl;
228
+ (_c = savedRangeRef.current) == null ? void 0 : _c.insertNode(a);
229
+ }
230
+ setLinkBar(false);
231
+ setLinkUrl("https://");
232
+ refresh();
233
+ };
234
+ const insertCodeBlock = () => {
235
+ const sel = window.getSelection();
236
+ if (!(sel == null ? void 0 : sel.rangeCount)) return;
237
+ const range = sel.getRangeAt(0);
238
+ const pre = document.createElement("pre");
239
+ const code = document.createElement("code");
240
+ code.textContent = range.toString() || "// code here";
241
+ pre.appendChild(code);
242
+ range.deleteContents();
243
+ range.insertNode(pre);
244
+ const br = document.createElement("br");
245
+ pre.insertAdjacentElement("afterend", br);
246
+ range.setStartAfter(br);
247
+ range.collapse(true);
248
+ sel.removeAllRanges();
249
+ sel.addRange(range);
250
+ refresh();
251
+ };
252
+ const copyHtml = () => {
253
+ var _a;
254
+ return navigator.clipboard.writeText(isCode ? codeVal : ((_a = editorRef.current) == null ? void 0 : _a.innerHTML) || "").catch(() => {
255
+ });
256
+ };
257
+ useEffect(() => {
258
+ if (editorRef.current) {
259
+ editorRef.current.innerHTML = "<p>Start writing here...</p>";
260
+ refresh();
261
+ }
262
+ }, []);
263
+ return /* @__PURE__ */ React.createElement("div", { style: { padding: "1rem 0" } }, /* @__PURE__ */ React.createElement("style", null, CSS), /* @__PURE__ */ React.createElement("div", { style: { border: "1px solid #d8d8d8", borderRadius: 4, overflow: "hidden", boxShadow: "0 1px 3px rgba(0,0,0,0.06)" } }, /* @__PURE__ */ React.createElement("div", { className: "rte-toolbar" }, !isCode && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("bold"), title: "Bold (Ctrl+B)", active: fmt.bold, style: { fontWeight: 800, fontFamily: "Georgia,serif" } }, "B"), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("italic"), title: "Italic (Ctrl+I)", active: fmt.italic, style: { fontStyle: "italic", fontFamily: "Georgia,serif" } }, "I"), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("underline"), title: "Underline (Ctrl+U)", active: fmt.underline, style: { textDecoration: "underline" } }, "U"), /* @__PURE__ */ React.createElement(Btn, { onClick: () => {
264
+ var _a;
265
+ return (_a = colorRef.current) == null ? void 0 : _a.click();
266
+ }, title: "Text color", style: { flexDirection: "column", gap: 1, padding: "3px 6px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontSize: 13, fontWeight: 800, fontFamily: "Georgia,serif", lineHeight: 1, color: "#222" } }, "A"), /* @__PURE__ */ React.createElement("span", { className: "rte-swatch", style: { background: color } })), /* @__PURE__ */ React.createElement(
267
+ "input",
268
+ {
269
+ ref: colorRef,
270
+ type: "color",
271
+ value: color,
272
+ onChange: (e) => {
273
+ setColor(e.target.value);
274
+ exec("foreColor", e.target.value);
275
+ },
276
+ style: { position: "absolute", opacity: 0, width: 0, height: 0, pointerEvents: "none" }
277
+ }
278
+ ), /* @__PURE__ */ React.createElement(Sep, null), /* @__PURE__ */ React.createElement(
279
+ "select",
280
+ {
281
+ className: "rte-select",
282
+ value: fmt.block || "p",
283
+ onMouseDown: () => {
284
+ var _a;
285
+ return (_a = editorRef.current) == null ? void 0 : _a.focus();
286
+ },
287
+ onChange: (e) => {
288
+ var _a;
289
+ (_a = editorRef.current) == null ? void 0 : _a.focus();
290
+ document.execCommand("formatBlock", false, e.target.value);
291
+ refresh();
292
+ }
293
+ },
294
+ /* @__PURE__ */ React.createElement("option", { value: "p" }, "Paragraph"),
295
+ /* @__PURE__ */ React.createElement("option", { value: "h1" }, "Heading 1"),
296
+ /* @__PURE__ */ React.createElement("option", { value: "h2" }, "Heading 2"),
297
+ /* @__PURE__ */ React.createElement("option", { value: "h3" }, "Heading 3")
298
+ ), /* @__PURE__ */ React.createElement(Sep, null), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("justifyLeft"), title: "Align left", active: fmt.aL }, /* @__PURE__ */ React.createElement(AlignIco, { t: "left" })), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("justifyCenter"), title: "Align center", active: fmt.aC }, /* @__PURE__ */ React.createElement(AlignIco, { t: "center" })), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("justifyRight"), title: "Align right", active: fmt.aR }, /* @__PURE__ */ React.createElement(AlignIco, { t: "right" })), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("justifyFull"), title: "Justify", active: fmt.aJ }, /* @__PURE__ */ React.createElement(AlignIco, { t: "justify" })), /* @__PURE__ */ React.createElement(Sep, null), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("insertUnorderedList"), title: "Bullet list", active: fmt.ul }, /* @__PURE__ */ React.createElement(IcoUL, null)), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("insertOrderedList"), title: "Numbered list", active: fmt.ol }, /* @__PURE__ */ React.createElement(IcoOL, null)), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("indent"), title: "Indent (Tab)" }, /* @__PURE__ */ React.createElement(IcoIndent, null)), /* @__PURE__ */ React.createElement(Btn, { onClick: () => exec("outdent"), title: "Outdent (Shift+Tab)" }, /* @__PURE__ */ React.createElement(IcoOutdent, null)), /* @__PURE__ */ React.createElement(Sep, null), /* @__PURE__ */ React.createElement(Btn, { onClick: openLinkBar, title: "Insert link", active: linkBar }, /* @__PURE__ */ React.createElement(IcoLink, null)), /* @__PURE__ */ React.createElement(Btn, { onClick: insertCodeBlock, title: "Code block" }, /* @__PURE__ */ React.createElement(IcoCode, null)), /* @__PURE__ */ React.createElement(Sep, null)), /* @__PURE__ */ React.createElement(
299
+ Btn,
300
+ {
301
+ onClick: isCode ? toVisual : toCode,
302
+ title: "Toggle HTML source",
303
+ active: isCode,
304
+ style: { fontSize: 12, fontWeight: 600, letterSpacing: "0.03em", padding: "0 8px" }
305
+ },
306
+ isCode ? "Visual" : "HTML"
307
+ ), /* @__PURE__ */ React.createElement(Btn, { onClick: copyHtml, title: "Copy HTML" }, /* @__PURE__ */ React.createElement(IcoCopy, null)), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12, color: "#aaa" } }, words, " ", words === 1 ? "word" : "words")), linkBar && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6, padding: "6px 10px", background: "#f0f4ff", borderBottom: "1px solid #c8d8f8" } }, /* @__PURE__ */ React.createElement(IcoLink, null), /* @__PURE__ */ React.createElement(
308
+ "input",
309
+ {
310
+ autoFocus: true,
311
+ type: "text",
312
+ value: linkUrl,
313
+ onChange: (e) => setLinkUrl(e.target.value),
314
+ onKeyDown: (e) => {
315
+ if (e.key === "Enter") applyLink();
316
+ if (e.key === "Escape") setLinkBar(false);
317
+ },
318
+ placeholder: "https://example.com",
319
+ style: { flex: 1, height: 26, border: "1px solid #b0c4f0", borderRadius: 3, padding: "0 8px", fontSize: 13, outline: "none", fontFamily: "var(--font-sans)" }
320
+ }
321
+ ), /* @__PURE__ */ React.createElement(
322
+ "button",
323
+ {
324
+ onClick: applyLink,
325
+ style: { height: 26, padding: "0 12px", background: "#1a6fc4", color: "#fff", border: "none", borderRadius: 3, fontSize: 12, fontWeight: 600, cursor: "pointer" }
326
+ },
327
+ "Apply"
328
+ ), /* @__PURE__ */ React.createElement(
329
+ "button",
330
+ {
331
+ onClick: () => setLinkBar(false),
332
+ style: { height: 26, padding: "0 10px", background: "transparent", color: "#666", border: "1px solid #ccc", borderRadius: 3, fontSize: 12, cursor: "pointer" }
333
+ },
334
+ "Cancel"
335
+ )), /* @__PURE__ */ React.createElement("div", { className: "rte-wrap" }, /* @__PURE__ */ React.createElement(
336
+ "textarea",
337
+ {
338
+ className: "rte-code",
339
+ value: codeVal,
340
+ onChange: (e) => setCodeVal(e.target.value),
341
+ spellCheck: false,
342
+ style: { display: isCode ? "block" : "none" }
343
+ }
344
+ ), /* @__PURE__ */ React.createElement(
345
+ "div",
346
+ {
347
+ ref: editorRef,
348
+ className: "rte-body",
349
+ contentEditable: true,
350
+ suppressContentEditableWarning: true,
351
+ "data-ph": "Start typing...",
352
+ onKeyDown: handleKeyDown,
353
+ onKeyUp: refresh,
354
+ onMouseUp: refresh,
355
+ onSelect: refresh,
356
+ style: { display: isCode ? "none" : "block" }
357
+ }
358
+ )), !isCode && /* @__PURE__ */ React.createElement("div", { className: "rte-footer" }, "Inside a list: ", /* @__PURE__ */ React.createElement("strong", null, "Tab"), " to go deeper \xA0\xB7\xA0 ", /* @__PURE__ */ React.createElement("strong", null, "Shift+Tab"), " to go back up")));
359
+ }
360
+ export {
361
+ RichTextEditor
362
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@malaya_jeeva/rich-text-editor",
3
+ "version": "1.0.0",
4
+ "description": "Custom React Rich Text Editor",
5
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.js",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsup"
12
+ },
13
+ "keywords": [
14
+ "react",
15
+ "rich-text-editor",
16
+ "wysiwyg",
17
+ "editor"
18
+ ],
19
+ "author": "",
20
+ "license": "ISC",
21
+ "peerDependencies": {
22
+ "react": "^18 || ^19",
23
+ "react-dom": "^18 || ^19"
24
+ },
25
+ "devDependencies": {
26
+ "react": "^19.2.4",
27
+ "react-dom": "^19.2.4",
28
+ "tsup": "^8.5.1",
29
+ "typescript": "^6.0.2"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }