@x-plat/design-system 0.10.0 → 0.12.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/components/AutoResizeTextArea/index.cjs +14 -5
- package/dist/components/AutoResizeTextArea/index.css +15 -0
- package/dist/components/AutoResizeTextArea/index.d.cts +4 -0
- package/dist/components/AutoResizeTextArea/index.d.ts +4 -0
- package/dist/components/AutoResizeTextArea/index.js +14 -5
- package/dist/components/Badge/index.d.cts +1 -1
- package/dist/components/Badge/index.d.ts +1 -1
- package/dist/components/ChatInput/index.cjs +20 -7
- package/dist/components/ChatInput/index.css +15 -0
- package/dist/components/ChatInput/index.d.cts +4 -0
- package/dist/components/ChatInput/index.d.ts +4 -0
- package/dist/components/ChatInput/index.js +20 -7
- package/dist/components/CheckBox/index.cjs +8 -3
- package/dist/components/CheckBox/index.css +15 -0
- package/dist/components/CheckBox/index.d.cts +1 -1
- package/dist/components/CheckBox/index.d.ts +1 -1
- package/dist/components/CheckBox/index.js +8 -3
- package/dist/components/DatePicker/index.cjs +179 -64
- package/dist/components/DatePicker/index.css +19 -0
- package/dist/components/DatePicker/index.js +179 -64
- package/dist/components/Drawer/index.cjs +83 -4
- package/dist/components/Drawer/index.js +83 -4
- package/dist/components/Dropdown/index.cjs +57 -16
- package/dist/components/Dropdown/index.css +3 -0
- package/dist/components/Dropdown/index.js +57 -16
- package/dist/components/Editor/index.cjs +779 -50
- package/dist/components/Editor/index.css +100 -0
- package/dist/components/Editor/index.d.cts +1 -1
- package/dist/components/Editor/index.d.ts +1 -1
- package/dist/components/Editor/index.js +779 -50
- package/dist/components/FieldMessage/index.cjs +8 -3
- package/dist/components/FieldMessage/index.css +15 -0
- package/dist/components/FieldMessage/index.d.cts +11 -4
- package/dist/components/FieldMessage/index.d.ts +11 -4
- package/dist/components/FieldMessage/index.js +8 -3
- package/dist/components/FileUpload/index.cjs +8 -3
- package/dist/components/FileUpload/index.css +15 -0
- package/dist/components/FileUpload/index.js +8 -3
- package/dist/components/ImageSelector/index.cjs +8 -3
- package/dist/components/ImageSelector/index.css +15 -0
- package/dist/components/ImageSelector/index.js +8 -3
- package/dist/components/Input/index.cjs +15 -5
- package/dist/components/Input/index.css +15 -0
- package/dist/components/Input/index.d.cts +4 -0
- package/dist/components/Input/index.d.ts +4 -0
- package/dist/components/Input/index.js +15 -5
- package/dist/components/Modal/index.cjs +84 -5
- package/dist/components/Modal/index.js +84 -5
- package/dist/components/NotificationBadge/index.d.cts +1 -1
- package/dist/components/NotificationBadge/index.d.ts +1 -1
- package/dist/components/Pagination/index.d.cts +1 -1
- package/dist/components/Pagination/index.d.ts +1 -1
- package/dist/components/PopOver/index.cjs +55 -13
- package/dist/components/PopOver/index.css +3 -0
- package/dist/components/PopOver/index.js +55 -13
- package/dist/components/Progress/index.d.cts +1 -1
- package/dist/components/Progress/index.d.ts +1 -1
- package/dist/components/Radio/index.cjs +8 -3
- package/dist/components/Radio/index.css +15 -0
- package/dist/components/Radio/index.d.cts +1 -1
- package/dist/components/Radio/index.d.ts +1 -1
- package/dist/components/Radio/index.js +8 -3
- package/dist/components/Select/index.cjs +120 -39
- package/dist/components/Select/index.css +57 -0
- package/dist/components/Select/index.d.cts +11 -0
- package/dist/components/Select/index.d.ts +11 -0
- package/dist/components/Select/index.js +120 -39
- package/dist/components/Spinner/index.d.cts +1 -1
- package/dist/components/Spinner/index.d.ts +1 -1
- package/dist/components/Stat/index.cjs +84 -0
- package/dist/components/Stat/index.css +96 -0
- package/dist/components/Stat/index.d.cts +37 -0
- package/dist/components/Stat/index.d.ts +37 -0
- package/dist/components/Stat/index.js +57 -0
- package/dist/components/Steps/index.d.cts +1 -1
- package/dist/components/Steps/index.d.ts +1 -1
- package/dist/components/Switch/index.cjs +14 -5
- package/dist/components/Switch/index.css +15 -0
- package/dist/components/Switch/index.d.cts +6 -1
- package/dist/components/Switch/index.d.ts +6 -1
- package/dist/components/Switch/index.js +14 -5
- package/dist/components/Tab/index.cjs +49 -25
- package/dist/components/Tab/index.css +4 -0
- package/dist/components/Tab/index.js +49 -25
- package/dist/components/TextArea/index.cjs +14 -4
- package/dist/components/TextArea/index.css +15 -0
- package/dist/components/TextArea/index.d.cts +4 -0
- package/dist/components/TextArea/index.d.ts +4 -0
- package/dist/components/TextArea/index.js +14 -4
- package/dist/components/TimePicker/index.cjs +461 -0
- package/dist/components/TimePicker/index.css +175 -0
- package/dist/components/TimePicker/index.d.cts +42 -0
- package/dist/components/TimePicker/index.d.ts +42 -0
- package/dist/components/TimePicker/index.js +424 -0
- package/dist/components/index.cjs +1500 -398
- package/dist/components/index.css +385 -0
- package/dist/components/index.d.cts +3 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.js +1498 -398
- package/dist/index.cjs +1524 -422
- package/dist/index.css +385 -0
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1522 -422
- package/dist/status-njWqQWFV.d.cts +20 -0
- package/dist/status-njWqQWFV.d.ts +20 -0
- package/guidelines/AGENT_PROMPT.md +3 -3
- package/guidelines/Guidelines.md +3 -0
- package/guidelines/components/editor.md +141 -0
- package/guidelines/components/form.md +19 -0
- package/guidelines/components/stat.md +31 -0
- package/guidelines/components/time-picker.md +49 -0
- package/package.json +1 -1
- package/dist/status-g_KP3qT6.d.cts +0 -8
- package/dist/status-g_KP3qT6.d.ts +0 -8
|
@@ -17,6 +17,109 @@ function clsx() {
|
|
|
17
17
|
}
|
|
18
18
|
var clsx_default = clsx;
|
|
19
19
|
|
|
20
|
+
// src/components/Editor/highlight.ts
|
|
21
|
+
var CODE_LANGUAGES = [
|
|
22
|
+
{ value: "plain", label: "\uC5C6\uC74C" },
|
|
23
|
+
{ value: "javascript", label: "JavaScript" },
|
|
24
|
+
{ value: "typescript", label: "TypeScript" },
|
|
25
|
+
{ value: "json", label: "JSON" },
|
|
26
|
+
{ value: "html", label: "HTML" },
|
|
27
|
+
{ value: "css", label: "CSS" },
|
|
28
|
+
{ value: "python", label: "Python" },
|
|
29
|
+
{ value: "sql", label: "SQL" },
|
|
30
|
+
{ value: "bash", label: "Bash" }
|
|
31
|
+
];
|
|
32
|
+
var KEYWORDS = {
|
|
33
|
+
plain: [],
|
|
34
|
+
javascript: "const let var function return if else for while class new this import export from default async await try catch throw typeof instanceof null undefined true false break continue switch case do delete in of yield".split(" "),
|
|
35
|
+
typescript: "const let var function return if else for while class new this import export from default async await try catch throw typeof instanceof null undefined true false break continue switch case do delete in of yield interface type enum implements extends public private protected readonly as satisfies keyof infer never unknown any".split(" "),
|
|
36
|
+
json: ["true", "false", "null"],
|
|
37
|
+
html: [],
|
|
38
|
+
css: [],
|
|
39
|
+
python: "def class return if elif else for while import from as try except finally raise with lambda None True False and or not in is pass break continue global nonlocal yield async await".split(" "),
|
|
40
|
+
sql: "SELECT FROM WHERE INSERT INTO VALUES UPDATE SET DELETE CREATE TABLE ALTER DROP JOIN LEFT RIGHT INNER OUTER ON GROUP BY ORDER HAVING LIMIT OFFSET AS AND OR NOT NULL DISTINCT UNION INDEX PRIMARY KEY FOREIGN REFERENCES DEFAULT".split(" "),
|
|
41
|
+
bash: "if then else fi for while do done case esac function return export local echo cd ls rm cp mv mkdir sudo source set unset exit".split(" ")
|
|
42
|
+
};
|
|
43
|
+
var LINE_COMMENT = {
|
|
44
|
+
javascript: "//",
|
|
45
|
+
typescript: "//",
|
|
46
|
+
css: "",
|
|
47
|
+
python: "#",
|
|
48
|
+
sql: "--",
|
|
49
|
+
bash: "#"
|
|
50
|
+
};
|
|
51
|
+
var escape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
52
|
+
var wrap = (cls, text) => `<span class="tok-${cls}">${escape(text)}</span>`;
|
|
53
|
+
var highlight = (code, lang) => {
|
|
54
|
+
if (lang === "plain" || !code) return escape(code);
|
|
55
|
+
const keywords = new Set(KEYWORDS[lang]);
|
|
56
|
+
const lineComment = LINE_COMMENT[lang];
|
|
57
|
+
const caseInsensitive = lang === "sql";
|
|
58
|
+
let out = "";
|
|
59
|
+
let i = 0;
|
|
60
|
+
while (i < code.length) {
|
|
61
|
+
const rest = code.slice(i);
|
|
62
|
+
if ((lang === "javascript" || lang === "typescript" || lang === "css") && rest.startsWith("/*")) {
|
|
63
|
+
const end = code.indexOf("*/", i + 2);
|
|
64
|
+
const stop = end === -1 ? code.length : end + 2;
|
|
65
|
+
out += wrap("comment", code.slice(i, stop));
|
|
66
|
+
i = stop;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (lineComment && rest.startsWith(lineComment)) {
|
|
70
|
+
const nl = code.indexOf("\n", i);
|
|
71
|
+
const stop = nl === -1 ? code.length : nl;
|
|
72
|
+
out += wrap("comment", code.slice(i, stop));
|
|
73
|
+
i = stop;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const q = rest[0];
|
|
77
|
+
if (q === '"' || q === "'" || q === "`") {
|
|
78
|
+
let j = 1;
|
|
79
|
+
while (j < rest.length) {
|
|
80
|
+
if (rest[j] === "\\") {
|
|
81
|
+
j += 2;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (rest[j] === q) {
|
|
85
|
+
j += 1;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (rest[j] === "\n" && q !== "`") break;
|
|
89
|
+
j += 1;
|
|
90
|
+
}
|
|
91
|
+
out += wrap("string", rest.slice(0, j));
|
|
92
|
+
i += j;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const num = /^\d+(\.\d+)?/.exec(rest);
|
|
96
|
+
if (num && !/[\w$]/.test(code[i - 1] ?? "")) {
|
|
97
|
+
out += wrap("number", num[0]);
|
|
98
|
+
i += num[0].length;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const word = /^[A-Za-z_$][\w$]*/.exec(rest);
|
|
102
|
+
if (word) {
|
|
103
|
+
const w = word[0];
|
|
104
|
+
const hit = caseInsensitive ? keywords.has(w.toUpperCase()) : keywords.has(w);
|
|
105
|
+
out += hit ? wrap("keyword", w) : escape(w);
|
|
106
|
+
i += w.length;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (lang === "html" && rest.startsWith("<")) {
|
|
110
|
+
const tag = /^<\/?[A-Za-z][\w-]*/.exec(rest);
|
|
111
|
+
if (tag) {
|
|
112
|
+
out += wrap("keyword", tag[0]);
|
|
113
|
+
i += tag[0].length;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
out += escape(code[i]);
|
|
118
|
+
i += 1;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
};
|
|
122
|
+
|
|
20
123
|
// src/components/Editor/Editor.tsx
|
|
21
124
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
22
125
|
var DEFAULT_TOOLBAR = [
|
|
@@ -25,14 +128,27 @@ var DEFAULT_TOOLBAR = [
|
|
|
25
128
|
"underline",
|
|
26
129
|
"strikethrough",
|
|
27
130
|
"code",
|
|
28
|
-
"heading",
|
|
131
|
+
"heading-1",
|
|
132
|
+
"heading-2",
|
|
133
|
+
"heading-3",
|
|
29
134
|
"list",
|
|
30
135
|
"ordered-list",
|
|
31
136
|
"blockquote",
|
|
32
137
|
"code-block",
|
|
33
138
|
"link",
|
|
34
139
|
"image",
|
|
35
|
-
"divider"
|
|
140
|
+
"divider",
|
|
141
|
+
"checklist",
|
|
142
|
+
"table",
|
|
143
|
+
"align-left",
|
|
144
|
+
"align-center",
|
|
145
|
+
"align-right",
|
|
146
|
+
"highlight",
|
|
147
|
+
"superscript",
|
|
148
|
+
"subscript",
|
|
149
|
+
"clear-format",
|
|
150
|
+
"undo",
|
|
151
|
+
"redo"
|
|
36
152
|
];
|
|
37
153
|
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
38
154
|
"p",
|
|
@@ -61,12 +177,51 @@ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
|
61
177
|
"img",
|
|
62
178
|
"hr",
|
|
63
179
|
"span",
|
|
64
|
-
"div"
|
|
180
|
+
"div",
|
|
181
|
+
"sub",
|
|
182
|
+
"sup",
|
|
183
|
+
"mark",
|
|
184
|
+
"table",
|
|
185
|
+
"thead",
|
|
186
|
+
"tbody",
|
|
187
|
+
"tr",
|
|
188
|
+
"th",
|
|
189
|
+
"td"
|
|
65
190
|
]);
|
|
66
191
|
var ALLOWED_ATTRS = {
|
|
67
192
|
a: ["href", "target", "rel"],
|
|
68
|
-
img: ["src", "alt"]
|
|
193
|
+
img: ["src", "alt", "width"],
|
|
194
|
+
// 정렬·체크리스트는 클래스로만 표현한다. style 을 열면 붙여넣기로 아무 CSS 나 들어온다
|
|
195
|
+
p: ["class"],
|
|
196
|
+
h1: ["class"],
|
|
197
|
+
h2: ["class"],
|
|
198
|
+
h3: ["class"],
|
|
199
|
+
h4: ["class"],
|
|
200
|
+
h5: ["class"],
|
|
201
|
+
h6: ["class"],
|
|
202
|
+
ul: ["class"],
|
|
203
|
+
ol: ["class"],
|
|
204
|
+
li: ["class", "data-checked"],
|
|
205
|
+
blockquote: ["class"],
|
|
206
|
+
div: ["class"],
|
|
207
|
+
span: ["class"],
|
|
208
|
+
pre: ["class"],
|
|
209
|
+
code: ["class"],
|
|
210
|
+
mark: ["class"],
|
|
211
|
+
table: ["class"],
|
|
212
|
+
td: ["class"],
|
|
213
|
+
th: ["class"],
|
|
214
|
+
tr: ["class"]
|
|
69
215
|
};
|
|
216
|
+
var DROP_WITH_CONTENT = /* @__PURE__ */ new Set([
|
|
217
|
+
"script",
|
|
218
|
+
"style",
|
|
219
|
+
"noscript",
|
|
220
|
+
"iframe",
|
|
221
|
+
"object",
|
|
222
|
+
"embed",
|
|
223
|
+
"template"
|
|
224
|
+
]);
|
|
70
225
|
var sanitizeHtml = (input) => {
|
|
71
226
|
const doc = new DOMParser().parseFromString(`<div>${input}</div>`, "text/html");
|
|
72
227
|
const root = doc.body.firstElementChild;
|
|
@@ -75,6 +230,10 @@ var sanitizeHtml = (input) => {
|
|
|
75
230
|
const children = Array.from(node.children);
|
|
76
231
|
for (const child of children) {
|
|
77
232
|
const tag = child.tagName.toLowerCase();
|
|
233
|
+
if (DROP_WITH_CONTENT.has(tag)) {
|
|
234
|
+
node.removeChild(child);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
78
237
|
if (!ALLOWED_TAGS.has(tag)) {
|
|
79
238
|
while (child.firstChild) node.insertBefore(child.firstChild, child);
|
|
80
239
|
node.removeChild(child);
|
|
@@ -105,7 +264,9 @@ var escapeHtml = (text) => {
|
|
|
105
264
|
};
|
|
106
265
|
var SLASH_ITEMS = [
|
|
107
266
|
{ key: "paragraph", label: "\uB2E8\uB77D", hint: "\uAE30\uBCF8 \uD14D\uC2A4\uD2B8" },
|
|
108
|
-
{ key: "heading", label: "\uC81C\uBAA9 1", hint: "\uD070 \uC81C\uBAA9" },
|
|
267
|
+
{ key: "heading-1", label: "\uC81C\uBAA9 1", hint: "\uAC00\uC7A5 \uD070 \uC81C\uBAA9" },
|
|
268
|
+
{ key: "heading-2", label: "\uC81C\uBAA9 2", hint: "" },
|
|
269
|
+
{ key: "heading-3", label: "\uC81C\uBAA9 3", hint: "" },
|
|
109
270
|
{ key: "list", label: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D", hint: "\u2022 \uD56D\uBAA9" },
|
|
110
271
|
{ key: "ordered-list", label: "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D", hint: "1. \uD56D\uBAA9" },
|
|
111
272
|
{ key: "blockquote", label: "\uC778\uC6A9", hint: "" },
|
|
@@ -139,12 +300,18 @@ var Editor = (props) => {
|
|
|
139
300
|
const [linkOpen, setLinkOpen] = React.useState(false);
|
|
140
301
|
const [linkValue, setLinkValue] = React.useState("");
|
|
141
302
|
const [linkPos, setLinkPos] = React.useState({ top: 0, left: 0 });
|
|
303
|
+
const lastEmittedRef = React.useRef(null);
|
|
142
304
|
React.useEffect(() => {
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
305
|
+
const el = editorRef.current;
|
|
306
|
+
if (!el) return;
|
|
307
|
+
if (value === lastEmittedRef.current) return;
|
|
308
|
+
if (el.innerHTML === value) return;
|
|
309
|
+
el.innerHTML = sanitizeHtml(value || "");
|
|
310
|
+
if (!el.firstChild) el.innerHTML = "<p><br></p>";
|
|
311
|
+
lastEmittedRef.current = null;
|
|
312
|
+
historyRef.current = [{ html: el.innerHTML, caret: 0 }];
|
|
313
|
+
historyIdxRef.current = 0;
|
|
314
|
+
updateEmpty();
|
|
148
315
|
}, [value]);
|
|
149
316
|
const updateEmpty = () => {
|
|
150
317
|
const el = editorRef.current;
|
|
@@ -160,10 +327,19 @@ var Editor = (props) => {
|
|
|
160
327
|
}
|
|
161
328
|
};
|
|
162
329
|
const restoreSelection = () => {
|
|
163
|
-
|
|
330
|
+
const range = lastRangeRef.current;
|
|
331
|
+
if (!range) return;
|
|
332
|
+
if (!editorRef.current?.contains(range.startContainer)) {
|
|
333
|
+
lastRangeRef.current = null;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
164
336
|
const sel = window.getSelection();
|
|
165
|
-
|
|
166
|
-
|
|
337
|
+
try {
|
|
338
|
+
sel?.removeAllRanges();
|
|
339
|
+
sel?.addRange(range);
|
|
340
|
+
} catch {
|
|
341
|
+
lastRangeRef.current = null;
|
|
342
|
+
}
|
|
167
343
|
};
|
|
168
344
|
const updateActiveFormats = () => {
|
|
169
345
|
if (!editorRef.current) return;
|
|
@@ -179,6 +355,17 @@ var Editor = (props) => {
|
|
|
179
355
|
if (document.queryCommandState("insertOrderedList")) next.add("ordered-list");
|
|
180
356
|
const block = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
|
|
181
357
|
if (block) next.add(`block:${block}`);
|
|
358
|
+
if (document.queryCommandState("superscript")) next.add("superscript");
|
|
359
|
+
if (document.queryCommandState("subscript")) next.add("subscript");
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const blockEl = currentBlock();
|
|
364
|
+
if (blockEl?.classList.contains("align-center")) next.add("align:center");
|
|
365
|
+
else if (blockEl?.classList.contains("align-right")) next.add("align:right");
|
|
366
|
+
else if (blockEl) next.add("align:left");
|
|
367
|
+
if (blockEl?.closest("mark")) next.add("highlight");
|
|
368
|
+
if (blockEl?.closest("ul.checklist")) next.add("checklist");
|
|
182
369
|
} catch {
|
|
183
370
|
}
|
|
184
371
|
setActiveFormats(next);
|
|
@@ -192,7 +379,374 @@ var Editor = (props) => {
|
|
|
192
379
|
const setBlock = (tag) => {
|
|
193
380
|
exec("formatBlock", tag);
|
|
194
381
|
};
|
|
382
|
+
const toggleBlock = (tag) => {
|
|
383
|
+
const current = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
|
|
384
|
+
setBlock(current === tag ? "p" : tag);
|
|
385
|
+
};
|
|
386
|
+
const historyRef = React.useRef([]);
|
|
387
|
+
const historyIdxRef = React.useRef(-1);
|
|
388
|
+
const lastPushAtRef = React.useRef(0);
|
|
389
|
+
const isRestoringRef = React.useRef(false);
|
|
390
|
+
const caretOffset = (rootEl) => {
|
|
391
|
+
const el = rootEl ?? editorRef.current;
|
|
392
|
+
const sel = window.getSelection();
|
|
393
|
+
if (!el || !sel || sel.rangeCount === 0 || !el.contains(sel.anchorNode)) return 0;
|
|
394
|
+
const range = sel.getRangeAt(0).cloneRange();
|
|
395
|
+
range.selectNodeContents(el);
|
|
396
|
+
range.setEnd(sel.getRangeAt(0).endContainer, sel.getRangeAt(0).endOffset);
|
|
397
|
+
return range.toString().length;
|
|
398
|
+
};
|
|
399
|
+
const setCaretOffset = (offset, rootEl) => {
|
|
400
|
+
const el = rootEl ?? editorRef.current;
|
|
401
|
+
if (!el) return;
|
|
402
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
403
|
+
let remaining = offset;
|
|
404
|
+
let node = null;
|
|
405
|
+
while (node = walker.nextNode()) {
|
|
406
|
+
const len = node.textContent?.length ?? 0;
|
|
407
|
+
if (remaining <= len) {
|
|
408
|
+
const range = document.createRange();
|
|
409
|
+
range.setStart(node, remaining);
|
|
410
|
+
range.collapse(true);
|
|
411
|
+
const sel = window.getSelection();
|
|
412
|
+
sel?.removeAllRanges();
|
|
413
|
+
sel?.addRange(range);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
remaining -= len;
|
|
417
|
+
}
|
|
418
|
+
placeCaretAtEnd(el);
|
|
419
|
+
};
|
|
420
|
+
const pushHistory = (force = false) => {
|
|
421
|
+
const el = editorRef.current;
|
|
422
|
+
if (!el || isRestoringRef.current) return;
|
|
423
|
+
const html = el.innerHTML;
|
|
424
|
+
const hist = historyRef.current;
|
|
425
|
+
const cur = hist[historyIdxRef.current];
|
|
426
|
+
if (cur && cur.html === html) return;
|
|
427
|
+
const now = Date.now();
|
|
428
|
+
const coalesce = !force && cur && now - lastPushAtRef.current < 500;
|
|
429
|
+
lastPushAtRef.current = now;
|
|
430
|
+
if (coalesce) {
|
|
431
|
+
hist[historyIdxRef.current] = { html, caret: caretOffset() };
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
hist.length = historyIdxRef.current + 1;
|
|
435
|
+
hist.push({ html, caret: caretOffset() });
|
|
436
|
+
if (hist.length > 200) hist.shift();
|
|
437
|
+
historyIdxRef.current = hist.length - 1;
|
|
438
|
+
};
|
|
439
|
+
const applySnapshot = (snap) => {
|
|
440
|
+
const el = editorRef.current;
|
|
441
|
+
if (!el) return;
|
|
442
|
+
isRestoringRef.current = true;
|
|
443
|
+
el.innerHTML = snap.html;
|
|
444
|
+
setCaretOffset(snap.caret);
|
|
445
|
+
isRestoringRef.current = false;
|
|
446
|
+
lastEmittedRef.current = el.innerHTML;
|
|
447
|
+
onChange?.(el.innerHTML);
|
|
448
|
+
updateEmpty();
|
|
449
|
+
updateActiveFormats();
|
|
450
|
+
};
|
|
451
|
+
const undo = () => {
|
|
452
|
+
pushHistory(true);
|
|
453
|
+
if (historyIdxRef.current <= 0) return;
|
|
454
|
+
historyIdxRef.current -= 1;
|
|
455
|
+
applySnapshot(historyRef.current[historyIdxRef.current]);
|
|
456
|
+
};
|
|
457
|
+
const redo = () => {
|
|
458
|
+
if (historyIdxRef.current >= historyRef.current.length - 1) return;
|
|
459
|
+
historyIdxRef.current += 1;
|
|
460
|
+
applySnapshot(historyRef.current[historyIdxRef.current]);
|
|
461
|
+
};
|
|
462
|
+
const langOf = (pre) => {
|
|
463
|
+
const cls = Array.from(pre.classList).find((c) => c.startsWith("lang-"));
|
|
464
|
+
return cls?.slice(5) ?? "plain";
|
|
465
|
+
};
|
|
466
|
+
const [codeLang, setCodeLang] = React.useState("plain");
|
|
467
|
+
const highlightRafRef = React.useRef(0);
|
|
468
|
+
const rehighlight = (pre) => {
|
|
469
|
+
if (composingRef.current) return;
|
|
470
|
+
const code = pre.querySelector("code");
|
|
471
|
+
if (!code) return;
|
|
472
|
+
const lang = langOf(pre);
|
|
473
|
+
const text = code.textContent || "";
|
|
474
|
+
const next = highlight(text, lang);
|
|
475
|
+
if (code.innerHTML === next) return;
|
|
476
|
+
const caret = caretOffset(code);
|
|
477
|
+
isRestoringRef.current = true;
|
|
478
|
+
code.innerHTML = next;
|
|
479
|
+
setCaretOffset(caret, code);
|
|
480
|
+
isRestoringRef.current = false;
|
|
481
|
+
};
|
|
482
|
+
const scheduleHighlight = () => {
|
|
483
|
+
const pre = closestPre();
|
|
484
|
+
if (!pre) return;
|
|
485
|
+
cancelAnimationFrame(highlightRafRef.current);
|
|
486
|
+
highlightRafRef.current = requestAnimationFrame(() => rehighlight(pre));
|
|
487
|
+
};
|
|
488
|
+
const changeCodeLang = (lang) => {
|
|
489
|
+
const pre = closestPre();
|
|
490
|
+
if (!pre) return;
|
|
491
|
+
pushHistory(true);
|
|
492
|
+
Array.from(pre.classList).filter((c) => c.startsWith("lang-")).forEach((c) => pre.classList.remove(c));
|
|
493
|
+
if (lang !== "plain") pre.classList.add(`lang-${lang}`);
|
|
494
|
+
setCodeLang(lang);
|
|
495
|
+
rehighlight(pre);
|
|
496
|
+
handleInput();
|
|
497
|
+
};
|
|
498
|
+
const closestPre = () => {
|
|
499
|
+
const sel = window.getSelection();
|
|
500
|
+
const node = sel?.anchorNode;
|
|
501
|
+
if (!node || !editorRef.current?.contains(node)) return null;
|
|
502
|
+
const el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
503
|
+
return el?.closest("pre") ?? null;
|
|
504
|
+
};
|
|
505
|
+
const insertCodeBlock = () => {
|
|
506
|
+
pushHistory(true);
|
|
507
|
+
const el = editorRef.current;
|
|
508
|
+
const sel = window.getSelection();
|
|
509
|
+
if (!el || !sel || sel.rangeCount === 0) return;
|
|
510
|
+
const existing = closestPre();
|
|
511
|
+
if (existing) {
|
|
512
|
+
const p = document.createElement("p");
|
|
513
|
+
p.textContent = existing.textContent || "";
|
|
514
|
+
if (!p.textContent) p.innerHTML = "<br>";
|
|
515
|
+
existing.replaceWith(p);
|
|
516
|
+
placeCaretAtEnd(p);
|
|
517
|
+
handleInput();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const range = sel.getRangeAt(0);
|
|
521
|
+
const frag = range.cloneContents();
|
|
522
|
+
const holder = document.createElement("div");
|
|
523
|
+
holder.appendChild(frag);
|
|
524
|
+
const BLOCK = /^(p|div|h[1-6]|li|blockquote|pre|tr)$/i;
|
|
525
|
+
const linesOf = (node) => {
|
|
526
|
+
const out = [];
|
|
527
|
+
let cur = "";
|
|
528
|
+
for (const child of Array.from(node.childNodes)) {
|
|
529
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
530
|
+
cur += child.textContent || "";
|
|
531
|
+
} else if (child.nodeName === "BR") {
|
|
532
|
+
out.push(cur);
|
|
533
|
+
cur = "";
|
|
534
|
+
} else if (BLOCK.test(child.nodeName)) {
|
|
535
|
+
if (cur) {
|
|
536
|
+
out.push(cur);
|
|
537
|
+
cur = "";
|
|
538
|
+
}
|
|
539
|
+
const sub = linesOf(child);
|
|
540
|
+
out.push(...sub.length > 0 ? sub : [""]);
|
|
541
|
+
} else {
|
|
542
|
+
cur += linesOf(child).join("\n");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (cur) out.push(cur);
|
|
546
|
+
return out;
|
|
547
|
+
};
|
|
548
|
+
const text = linesOf(holder).join("\n").replace(/\n+$/, "");
|
|
549
|
+
const pre = document.createElement("pre");
|
|
550
|
+
const code = document.createElement("code");
|
|
551
|
+
code.textContent = text || "";
|
|
552
|
+
pre.appendChild(code);
|
|
553
|
+
range.deleteContents();
|
|
554
|
+
range.insertNode(pre);
|
|
555
|
+
ensureParagraphAfter(pre);
|
|
556
|
+
placeCaretAtEnd(code);
|
|
557
|
+
handleInput();
|
|
558
|
+
};
|
|
559
|
+
const insertTextAtCaret = (text) => {
|
|
560
|
+
const sel = window.getSelection();
|
|
561
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
562
|
+
const range = sel.getRangeAt(0);
|
|
563
|
+
range.deleteContents();
|
|
564
|
+
const node = document.createTextNode(text);
|
|
565
|
+
range.insertNode(node);
|
|
566
|
+
range.setStartAfter(node);
|
|
567
|
+
range.collapse(true);
|
|
568
|
+
sel.removeAllRanges();
|
|
569
|
+
sel.addRange(range);
|
|
570
|
+
handleInput();
|
|
571
|
+
};
|
|
572
|
+
const ensureParagraphAfter = (node) => {
|
|
573
|
+
const next = node.nextElementSibling;
|
|
574
|
+
if (next && next.tagName === "P") return;
|
|
575
|
+
const p = document.createElement("p");
|
|
576
|
+
p.innerHTML = "<br>";
|
|
577
|
+
node.after(p);
|
|
578
|
+
};
|
|
579
|
+
const placeCaretAtEnd = (node) => {
|
|
580
|
+
const sel = window.getSelection();
|
|
581
|
+
if (!sel) return;
|
|
582
|
+
const r2 = document.createRange();
|
|
583
|
+
r2.selectNodeContents(node);
|
|
584
|
+
r2.collapse(false);
|
|
585
|
+
sel.removeAllRanges();
|
|
586
|
+
sel.addRange(r2);
|
|
587
|
+
editorRef.current?.focus();
|
|
588
|
+
};
|
|
589
|
+
const clearFormat = () => {
|
|
590
|
+
exec("removeFormat");
|
|
591
|
+
exec("unlink");
|
|
592
|
+
setBlock("p");
|
|
593
|
+
};
|
|
594
|
+
const indentList = (outdent) => {
|
|
595
|
+
exec(outdent ? "outdent" : "indent");
|
|
596
|
+
};
|
|
597
|
+
const inListItem = () => {
|
|
598
|
+
const sel = window.getSelection();
|
|
599
|
+
const node = sel?.anchorNode;
|
|
600
|
+
if (!node || !editorRef.current?.contains(node)) return false;
|
|
601
|
+
const el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
602
|
+
return !!el?.closest("li");
|
|
603
|
+
};
|
|
604
|
+
const currentBlock = () => {
|
|
605
|
+
const sel = window.getSelection();
|
|
606
|
+
const node = sel?.anchorNode;
|
|
607
|
+
const root = editorRef.current;
|
|
608
|
+
if (!node || !root?.contains(node)) return null;
|
|
609
|
+
let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
610
|
+
while (el && el !== root && !/^(P|H[1-6]|LI|BLOCKQUOTE|PRE|DIV|TD|TH)$/.test(el.tagName)) {
|
|
611
|
+
el = el.parentElement;
|
|
612
|
+
}
|
|
613
|
+
return el && el !== root ? el : null;
|
|
614
|
+
};
|
|
615
|
+
const setAlign = (align) => {
|
|
616
|
+
const block = currentBlock();
|
|
617
|
+
if (!block) return;
|
|
618
|
+
pushHistory(true);
|
|
619
|
+
block.classList.remove("align-left", "align-center", "align-right");
|
|
620
|
+
if (align !== "left") block.classList.add(`align-${align}`);
|
|
621
|
+
handleInput();
|
|
622
|
+
};
|
|
623
|
+
const toggleChecklist = () => {
|
|
624
|
+
pushHistory(true);
|
|
625
|
+
const block = currentBlock();
|
|
626
|
+
const list = block?.closest("ul");
|
|
627
|
+
if (list?.classList.contains("checklist")) {
|
|
628
|
+
list.classList.remove("checklist");
|
|
629
|
+
list.querySelectorAll("li").forEach((li) => li.removeAttribute("data-checked"));
|
|
630
|
+
handleInput();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (list) {
|
|
634
|
+
list.classList.add("checklist");
|
|
635
|
+
list.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
|
|
636
|
+
handleInput();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
exec("insertUnorderedList");
|
|
640
|
+
const made = currentBlock()?.closest("ul");
|
|
641
|
+
if (made) {
|
|
642
|
+
made.classList.add("checklist");
|
|
643
|
+
made.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
|
|
644
|
+
}
|
|
645
|
+
handleInput();
|
|
646
|
+
};
|
|
647
|
+
const handleEditorClick = (e) => {
|
|
648
|
+
if (readOnly) return;
|
|
649
|
+
const target = e.target;
|
|
650
|
+
const li = target.closest?.("li");
|
|
651
|
+
if (!li || !li.closest("ul.checklist")) return;
|
|
652
|
+
const rect = li.getBoundingClientRect();
|
|
653
|
+
if (e.clientX - rect.left > 20) return;
|
|
654
|
+
e.preventDefault();
|
|
655
|
+
pushHistory(true);
|
|
656
|
+
li.setAttribute("data-checked", li.getAttribute("data-checked") === "true" ? "false" : "true");
|
|
657
|
+
handleInput();
|
|
658
|
+
};
|
|
659
|
+
const toggleHighlight = () => {
|
|
660
|
+
const sel = window.getSelection();
|
|
661
|
+
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return;
|
|
662
|
+
pushHistory(true);
|
|
663
|
+
const range = sel.getRangeAt(0);
|
|
664
|
+
const node = sel.anchorNode;
|
|
665
|
+
const el = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
|
|
666
|
+
const existing = el?.closest("mark");
|
|
667
|
+
if (existing) {
|
|
668
|
+
const parent = existing.parentNode;
|
|
669
|
+
while (existing.firstChild) parent?.insertBefore(existing.firstChild, existing);
|
|
670
|
+
parent?.removeChild(existing);
|
|
671
|
+
handleInput();
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const mark = document.createElement("mark");
|
|
675
|
+
try {
|
|
676
|
+
range.surroundContents(mark);
|
|
677
|
+
} catch {
|
|
678
|
+
const frag = range.extractContents();
|
|
679
|
+
mark.appendChild(frag);
|
|
680
|
+
range.insertNode(mark);
|
|
681
|
+
}
|
|
682
|
+
handleInput();
|
|
683
|
+
};
|
|
684
|
+
const insertTable = (cols = 3, rows = 2) => {
|
|
685
|
+
const sel = window.getSelection();
|
|
686
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
687
|
+
pushHistory(true);
|
|
688
|
+
const table = document.createElement("table");
|
|
689
|
+
const thead = document.createElement("thead");
|
|
690
|
+
const headRow = document.createElement("tr");
|
|
691
|
+
for (let c = 0; c < cols; c++) {
|
|
692
|
+
const th = document.createElement("th");
|
|
693
|
+
th.innerHTML = "<br>";
|
|
694
|
+
headRow.appendChild(th);
|
|
695
|
+
}
|
|
696
|
+
thead.appendChild(headRow);
|
|
697
|
+
table.appendChild(thead);
|
|
698
|
+
const tbody = document.createElement("tbody");
|
|
699
|
+
for (let r2 = 0; r2 < rows; r2++) {
|
|
700
|
+
const tr = document.createElement("tr");
|
|
701
|
+
for (let c = 0; c < cols; c++) {
|
|
702
|
+
const td = document.createElement("td");
|
|
703
|
+
td.innerHTML = "<br>";
|
|
704
|
+
tr.appendChild(td);
|
|
705
|
+
}
|
|
706
|
+
tbody.appendChild(tr);
|
|
707
|
+
}
|
|
708
|
+
table.appendChild(tbody);
|
|
709
|
+
const range = sel.getRangeAt(0);
|
|
710
|
+
range.deleteContents();
|
|
711
|
+
range.insertNode(table);
|
|
712
|
+
ensureParagraphAfter(table);
|
|
713
|
+
const first = table.querySelector("th");
|
|
714
|
+
if (first) placeCaretAtEnd(first);
|
|
715
|
+
handleInput();
|
|
716
|
+
};
|
|
717
|
+
const moveTableCell = (backwards) => {
|
|
718
|
+
const block = currentBlock();
|
|
719
|
+
const cell = block?.closest("td, th");
|
|
720
|
+
if (!cell) return false;
|
|
721
|
+
const table = cell.closest("table");
|
|
722
|
+
if (!table) return false;
|
|
723
|
+
const cells = Array.from(table.querySelectorAll("td, th"));
|
|
724
|
+
const i = cells.indexOf(cell);
|
|
725
|
+
const next = cells[i + (backwards ? -1 : 1)];
|
|
726
|
+
if (next) {
|
|
727
|
+
placeCaretAtEnd(next);
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
if (!backwards) {
|
|
731
|
+
pushHistory(true);
|
|
732
|
+
const lastRow = table.querySelector("tbody")?.lastElementChild;
|
|
733
|
+
if (lastRow) {
|
|
734
|
+
const tr = document.createElement("tr");
|
|
735
|
+
for (let c = 0; c < lastRow.children.length; c++) {
|
|
736
|
+
const td = document.createElement("td");
|
|
737
|
+
td.innerHTML = "<br>";
|
|
738
|
+
tr.appendChild(td);
|
|
739
|
+
}
|
|
740
|
+
lastRow.after(tr);
|
|
741
|
+
placeCaretAtEnd(tr.firstElementChild);
|
|
742
|
+
handleInput();
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return false;
|
|
747
|
+
};
|
|
195
748
|
const insertDivider = () => {
|
|
749
|
+
pushHistory(true);
|
|
196
750
|
exec("insertHorizontalRule");
|
|
197
751
|
const el = editorRef.current;
|
|
198
752
|
if (el && el.lastElementChild?.tagName === "HR") {
|
|
@@ -238,7 +792,9 @@ var Editor = (props) => {
|
|
|
238
792
|
const range = sel.getRangeAt(0);
|
|
239
793
|
slashStartRangeRef.current = range.cloneRange();
|
|
240
794
|
const rect = range.getBoundingClientRect();
|
|
241
|
-
const
|
|
795
|
+
const editorEl = editorRef.current;
|
|
796
|
+
if (!editorEl) return;
|
|
797
|
+
const editorRect = editorEl.getBoundingClientRect();
|
|
242
798
|
setSlashPos({
|
|
243
799
|
top: rect.bottom - editorRect.top + 4,
|
|
244
800
|
left: rect.left - editorRect.left
|
|
@@ -283,8 +839,14 @@ var Editor = (props) => {
|
|
|
283
839
|
case "paragraph":
|
|
284
840
|
setBlock("p");
|
|
285
841
|
break;
|
|
286
|
-
case "heading":
|
|
287
|
-
|
|
842
|
+
case "heading-1":
|
|
843
|
+
toggleBlock("h1");
|
|
844
|
+
break;
|
|
845
|
+
case "heading-2":
|
|
846
|
+
toggleBlock("h2");
|
|
847
|
+
break;
|
|
848
|
+
case "heading-3":
|
|
849
|
+
toggleBlock("h3");
|
|
288
850
|
break;
|
|
289
851
|
case "list":
|
|
290
852
|
exec("insertUnorderedList");
|
|
@@ -296,7 +858,7 @@ var Editor = (props) => {
|
|
|
296
858
|
setBlock("blockquote");
|
|
297
859
|
break;
|
|
298
860
|
case "code-block":
|
|
299
|
-
|
|
861
|
+
insertCodeBlock();
|
|
300
862
|
break;
|
|
301
863
|
case "divider":
|
|
302
864
|
insertDivider();
|
|
@@ -314,7 +876,9 @@ var Editor = (props) => {
|
|
|
314
876
|
if (!sel || sel.rangeCount === 0) return;
|
|
315
877
|
const range = sel.getRangeAt(0);
|
|
316
878
|
const rect = range.getBoundingClientRect();
|
|
317
|
-
const
|
|
879
|
+
const editorEl2 = editorRef.current;
|
|
880
|
+
if (!editorEl2) return;
|
|
881
|
+
const editorRect = editorEl2.getBoundingClientRect();
|
|
318
882
|
setLinkPos({
|
|
319
883
|
top: rect.bottom - editorRect.top + 4,
|
|
320
884
|
left: rect.left - editorRect.left
|
|
@@ -347,13 +911,13 @@ var Editor = (props) => {
|
|
|
347
911
|
if (node.nodeType !== Node.TEXT_NODE) return false;
|
|
348
912
|
const text = (node.textContent || "").slice(0, range.startOffset);
|
|
349
913
|
const patterns = [
|
|
350
|
-
[
|
|
351
|
-
[
|
|
352
|
-
[
|
|
353
|
-
[
|
|
354
|
-
[
|
|
355
|
-
[/^1
|
|
356
|
-
[
|
|
914
|
+
[/^###$/, () => setBlock("h3")],
|
|
915
|
+
[/^##$/, () => setBlock("h2")],
|
|
916
|
+
[/^#$/, () => setBlock("h1")],
|
|
917
|
+
[/^-$/, () => exec("insertUnorderedList")],
|
|
918
|
+
[/^\*$/, () => exec("insertUnorderedList")],
|
|
919
|
+
[/^1\.$/, () => exec("insertOrderedList")],
|
|
920
|
+
[/^>$/, () => setBlock("blockquote")]
|
|
357
921
|
];
|
|
358
922
|
for (const [re, fn] of patterns) {
|
|
359
923
|
if (re.test(text)) {
|
|
@@ -373,7 +937,11 @@ var Editor = (props) => {
|
|
|
373
937
|
if (composingRef.current) return;
|
|
374
938
|
const el = editorRef.current;
|
|
375
939
|
if (!el) return;
|
|
376
|
-
|
|
940
|
+
const html = el.innerHTML;
|
|
941
|
+
lastEmittedRef.current = html;
|
|
942
|
+
pushHistory();
|
|
943
|
+
scheduleHighlight();
|
|
944
|
+
onChange?.(html);
|
|
377
945
|
updateEmpty();
|
|
378
946
|
updateActiveFormats();
|
|
379
947
|
if (slashOpen) {
|
|
@@ -401,6 +969,40 @@ var Editor = (props) => {
|
|
|
401
969
|
};
|
|
402
970
|
const handleKeyDown = (e) => {
|
|
403
971
|
if (readOnly) return;
|
|
972
|
+
if (composingRef.current || e.nativeEvent.isComposing || e.keyCode === 229) return;
|
|
973
|
+
const pre = closestPre();
|
|
974
|
+
if (pre && !slashOpen) {
|
|
975
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
976
|
+
const text = pre.textContent || "";
|
|
977
|
+
if (text.endsWith("\n\n")) {
|
|
978
|
+
e.preventDefault();
|
|
979
|
+
const code = pre.querySelector("code") ?? pre;
|
|
980
|
+
code.textContent = text.replace(/\n\n$/, "");
|
|
981
|
+
ensureParagraphAfter(pre);
|
|
982
|
+
const next = pre.nextElementSibling;
|
|
983
|
+
if (next) placeCaretAtEnd(next);
|
|
984
|
+
handleInput();
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
e.preventDefault();
|
|
988
|
+
insertTextAtCaret("\n");
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (e.key === "Tab") {
|
|
992
|
+
e.preventDefault();
|
|
993
|
+
insertTextAtCaret(" ");
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (e.key === "Tab" && moveTableCell(e.shiftKey)) {
|
|
998
|
+
e.preventDefault();
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (e.key === "Tab" && inListItem()) {
|
|
1002
|
+
e.preventDefault();
|
|
1003
|
+
indentList(e.shiftKey);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
404
1006
|
if (slashOpen) {
|
|
405
1007
|
if (e.key === "ArrowDown") {
|
|
406
1008
|
e.preventDefault();
|
|
@@ -424,6 +1026,21 @@ var Editor = (props) => {
|
|
|
424
1026
|
return;
|
|
425
1027
|
}
|
|
426
1028
|
}
|
|
1029
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
|
|
1030
|
+
e.preventDefault();
|
|
1031
|
+
if (e.shiftKey) redo();
|
|
1032
|
+
else undo();
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "v") {
|
|
1036
|
+
plainPasteRef.current = true;
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "y") {
|
|
1040
|
+
e.preventDefault();
|
|
1041
|
+
redo();
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
427
1044
|
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
|
|
428
1045
|
const k = e.key.toLowerCase();
|
|
429
1046
|
if (k === "b") {
|
|
@@ -447,6 +1064,7 @@ var Editor = (props) => {
|
|
|
447
1064
|
return;
|
|
448
1065
|
}
|
|
449
1066
|
}
|
|
1067
|
+
if (pre) return;
|
|
450
1068
|
if (enableSlashCommand && e.key === "/") {
|
|
451
1069
|
setTimeout(openSlashMenu, 0);
|
|
452
1070
|
}
|
|
@@ -456,11 +1074,21 @@ var Editor = (props) => {
|
|
|
456
1074
|
}
|
|
457
1075
|
}
|
|
458
1076
|
};
|
|
1077
|
+
const plainPasteRef = React.useRef(false);
|
|
459
1078
|
const handlePaste = (e) => {
|
|
460
1079
|
if (readOnly) return;
|
|
461
1080
|
e.preventDefault();
|
|
462
|
-
const html = e.clipboardData.getData("text/html");
|
|
463
1081
|
const text = e.clipboardData.getData("text/plain");
|
|
1082
|
+
if (plainPasteRef.current) {
|
|
1083
|
+
plainPasteRef.current = false;
|
|
1084
|
+
insertTextAtCaret(text);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
if (closestPre()) {
|
|
1088
|
+
insertTextAtCaret(text);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const html = e.clipboardData.getData("text/html");
|
|
464
1092
|
const clean = html ? sanitizeHtml(html) : escapeHtml(text);
|
|
465
1093
|
document.execCommand("insertHTML", false, clean);
|
|
466
1094
|
handleInput();
|
|
@@ -482,8 +1110,11 @@ var Editor = (props) => {
|
|
|
482
1110
|
};
|
|
483
1111
|
const handleSelectionUpdate = () => {
|
|
484
1112
|
updateActiveFormats();
|
|
1113
|
+
const pre = closestPre();
|
|
1114
|
+
setCodeLang(pre ? langOf(pre) : "plain");
|
|
485
1115
|
};
|
|
486
1116
|
const onToolbarAction = (item) => {
|
|
1117
|
+
if (closestPre() && !["code-block", "undo", "redo"].includes(item)) return;
|
|
487
1118
|
switch (item) {
|
|
488
1119
|
case "bold":
|
|
489
1120
|
exec("bold");
|
|
@@ -512,8 +1143,14 @@ var Editor = (props) => {
|
|
|
512
1143
|
handleInput();
|
|
513
1144
|
break;
|
|
514
1145
|
}
|
|
515
|
-
case "heading":
|
|
516
|
-
|
|
1146
|
+
case "heading-1":
|
|
1147
|
+
toggleBlock("h1");
|
|
1148
|
+
break;
|
|
1149
|
+
case "heading-2":
|
|
1150
|
+
toggleBlock("h2");
|
|
1151
|
+
break;
|
|
1152
|
+
case "heading-3":
|
|
1153
|
+
toggleBlock("h3");
|
|
517
1154
|
break;
|
|
518
1155
|
case "list":
|
|
519
1156
|
exec("insertUnorderedList");
|
|
@@ -525,7 +1162,7 @@ var Editor = (props) => {
|
|
|
525
1162
|
setBlock("blockquote");
|
|
526
1163
|
break;
|
|
527
1164
|
case "code-block":
|
|
528
|
-
|
|
1165
|
+
insertCodeBlock();
|
|
529
1166
|
break;
|
|
530
1167
|
case "link":
|
|
531
1168
|
openLinkEditor();
|
|
@@ -536,6 +1173,39 @@ var Editor = (props) => {
|
|
|
536
1173
|
case "divider":
|
|
537
1174
|
insertDivider();
|
|
538
1175
|
break;
|
|
1176
|
+
case "checklist":
|
|
1177
|
+
toggleChecklist();
|
|
1178
|
+
break;
|
|
1179
|
+
case "align-left":
|
|
1180
|
+
setAlign("left");
|
|
1181
|
+
break;
|
|
1182
|
+
case "align-center":
|
|
1183
|
+
setAlign("center");
|
|
1184
|
+
break;
|
|
1185
|
+
case "align-right":
|
|
1186
|
+
setAlign("right");
|
|
1187
|
+
break;
|
|
1188
|
+
case "highlight":
|
|
1189
|
+
toggleHighlight();
|
|
1190
|
+
break;
|
|
1191
|
+
case "superscript":
|
|
1192
|
+
exec("superscript");
|
|
1193
|
+
break;
|
|
1194
|
+
case "subscript":
|
|
1195
|
+
exec("subscript");
|
|
1196
|
+
break;
|
|
1197
|
+
case "table":
|
|
1198
|
+
insertTable();
|
|
1199
|
+
break;
|
|
1200
|
+
case "clear-format":
|
|
1201
|
+
clearFormat();
|
|
1202
|
+
break;
|
|
1203
|
+
case "undo":
|
|
1204
|
+
undo();
|
|
1205
|
+
break;
|
|
1206
|
+
case "redo":
|
|
1207
|
+
redo();
|
|
1208
|
+
break;
|
|
539
1209
|
}
|
|
540
1210
|
};
|
|
541
1211
|
return /* @__PURE__ */ jsxs("div", { className: clsx_default("lib-xplat-editor", isFocused && "focused", readOnly && "readonly"), children: [
|
|
@@ -544,7 +1214,10 @@ var Editor = (props) => {
|
|
|
544
1214
|
{
|
|
545
1215
|
items: toolbar,
|
|
546
1216
|
active: activeFormats,
|
|
547
|
-
onAction: onToolbarAction
|
|
1217
|
+
onAction: onToolbarAction,
|
|
1218
|
+
codeLang,
|
|
1219
|
+
inCodeBlock: activeFormats.has("block:pre"),
|
|
1220
|
+
onCodeLangChange: changeCodeLang
|
|
548
1221
|
}
|
|
549
1222
|
),
|
|
550
1223
|
/* @__PURE__ */ jsxs("div", { className: "lib-xplat-editor__content", style: { minHeight }, children: [
|
|
@@ -555,9 +1228,14 @@ var Editor = (props) => {
|
|
|
555
1228
|
ref: editorRef,
|
|
556
1229
|
contentEditable: !readOnly,
|
|
557
1230
|
suppressContentEditableWarning: true,
|
|
1231
|
+
role: "textbox",
|
|
1232
|
+
"aria-multiline": "true",
|
|
1233
|
+
"aria-readonly": readOnly || void 0,
|
|
1234
|
+
"aria-label": placeholder,
|
|
558
1235
|
className: "lib-xplat-editor__editable",
|
|
559
1236
|
onInput: handleInput,
|
|
560
1237
|
onKeyDown: handleKeyDown,
|
|
1238
|
+
onClick: handleEditorClick,
|
|
561
1239
|
onKeyUp: handleSelectionUpdate,
|
|
562
1240
|
onMouseUp: handleSelectionUpdate,
|
|
563
1241
|
onPaste: handlePaste,
|
|
@@ -599,14 +1277,27 @@ var TOOLBAR_LABEL = {
|
|
|
599
1277
|
underline: "U",
|
|
600
1278
|
strikethrough: "S",
|
|
601
1279
|
code: "<>",
|
|
602
|
-
heading: "
|
|
1280
|
+
"heading-1": "H1",
|
|
1281
|
+
"heading-2": "H2",
|
|
1282
|
+
"heading-3": "H3",
|
|
603
1283
|
list: "\u2022",
|
|
604
1284
|
"ordered-list": "1.",
|
|
605
1285
|
blockquote: "\u275D",
|
|
606
1286
|
"code-block": "[ ]",
|
|
607
1287
|
link: "\u{1F517}",
|
|
608
1288
|
image: "\u{1F5BC}",
|
|
609
|
-
divider: "\u2014"
|
|
1289
|
+
divider: "\u2014",
|
|
1290
|
+
checklist: "\u2611",
|
|
1291
|
+
"align-left": "\u21E4",
|
|
1292
|
+
"align-center": "\u2194",
|
|
1293
|
+
"align-right": "\u21E5",
|
|
1294
|
+
highlight: "\u{1F58D}",
|
|
1295
|
+
superscript: "x\xB2",
|
|
1296
|
+
subscript: "x\u2082",
|
|
1297
|
+
table: "\u25A6",
|
|
1298
|
+
"clear-format": "\u232B",
|
|
1299
|
+
undo: "\u21B6",
|
|
1300
|
+
redo: "\u21B7"
|
|
610
1301
|
};
|
|
611
1302
|
var TOOLBAR_TITLE = {
|
|
612
1303
|
bold: "\uAD75\uAC8C (\u2318B)",
|
|
@@ -614,36 +1305,74 @@ var TOOLBAR_TITLE = {
|
|
|
614
1305
|
underline: "\uBC11\uC904 (\u2318U)",
|
|
615
1306
|
strikethrough: "\uCDE8\uC18C\uC120",
|
|
616
1307
|
code: "\uC778\uB77C\uC778 \uCF54\uB4DC",
|
|
617
|
-
heading: "\uC81C\uBAA9",
|
|
1308
|
+
"heading-1": "\uC81C\uBAA9 1",
|
|
1309
|
+
"heading-2": "\uC81C\uBAA9 2",
|
|
1310
|
+
"heading-3": "\uC81C\uBAA9 3",
|
|
618
1311
|
list: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D",
|
|
619
1312
|
"ordered-list": "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D",
|
|
620
1313
|
blockquote: "\uC778\uC6A9",
|
|
621
1314
|
"code-block": "\uCF54\uB4DC \uBE14\uB85D",
|
|
622
1315
|
link: "\uB9C1\uD06C (\u2318K)",
|
|
623
1316
|
image: "\uC774\uBBF8\uC9C0",
|
|
624
|
-
divider: "\uAD6C\uBD84\uC120"
|
|
1317
|
+
divider: "\uAD6C\uBD84\uC120",
|
|
1318
|
+
checklist: "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8",
|
|
1319
|
+
"align-left": "\uC67C\uCABD \uC815\uB82C",
|
|
1320
|
+
"align-center": "\uAC00\uC6B4\uB370 \uC815\uB82C",
|
|
1321
|
+
"align-right": "\uC624\uB978\uCABD \uC815\uB82C",
|
|
1322
|
+
highlight: "\uD615\uAD11\uD39C",
|
|
1323
|
+
superscript: "\uC704 \uCCA8\uC790",
|
|
1324
|
+
subscript: "\uC544\uB798 \uCCA8\uC790",
|
|
1325
|
+
table: "\uD45C \uC0BD\uC785",
|
|
1326
|
+
"clear-format": "\uC11C\uC2DD \uC9C0\uC6B0\uAE30",
|
|
1327
|
+
undo: "\uB418\uB3CC\uB9AC\uAE30 (\u2318Z)",
|
|
1328
|
+
redo: "\uB2E4\uC2DC\uD558\uAE30 (\u2318\u21E7Z)"
|
|
625
1329
|
};
|
|
626
1330
|
var isActive = (item, active) => {
|
|
627
|
-
if (item === "heading") return active.has("block:h1")
|
|
1331
|
+
if (item === "heading-1") return active.has("block:h1");
|
|
1332
|
+
if (item === "heading-2") return active.has("block:h2");
|
|
1333
|
+
if (item === "heading-3") return active.has("block:h3");
|
|
628
1334
|
if (item === "blockquote") return active.has("block:blockquote");
|
|
629
1335
|
if (item === "code-block") return active.has("block:pre");
|
|
1336
|
+
if (item === "align-left") return active.has("align:left");
|
|
1337
|
+
if (item === "align-center") return active.has("align:center");
|
|
1338
|
+
if (item === "align-right") return active.has("align:right");
|
|
630
1339
|
return active.has(item);
|
|
631
1340
|
};
|
|
632
|
-
var EditorToolbar = ({
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1341
|
+
var EditorToolbar = ({
|
|
1342
|
+
items,
|
|
1343
|
+
active,
|
|
1344
|
+
onAction,
|
|
1345
|
+
codeLang,
|
|
1346
|
+
inCodeBlock,
|
|
1347
|
+
onCodeLangChange
|
|
1348
|
+
}) => {
|
|
1349
|
+
return /* @__PURE__ */ jsxs("div", { className: "lib-xplat-editor__toolbar", children: [
|
|
1350
|
+
items.map((item) => /* @__PURE__ */ jsx(
|
|
1351
|
+
"button",
|
|
1352
|
+
{
|
|
1353
|
+
type: "button",
|
|
1354
|
+
className: clsx_default("lib-xplat-editor__toolbar-btn", isActive(item, active) && "active"),
|
|
1355
|
+
title: TOOLBAR_TITLE[item],
|
|
1356
|
+
onMouseDown: (e) => {
|
|
1357
|
+
e.preventDefault();
|
|
1358
|
+
onAction(item);
|
|
1359
|
+
},
|
|
1360
|
+
children: TOOLBAR_LABEL[item]
|
|
642
1361
|
},
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
1362
|
+
item
|
|
1363
|
+
)),
|
|
1364
|
+
inCodeBlock && /* @__PURE__ */ jsx(
|
|
1365
|
+
"select",
|
|
1366
|
+
{
|
|
1367
|
+
className: "lib-xplat-editor__lang",
|
|
1368
|
+
value: codeLang,
|
|
1369
|
+
"aria-label": "\uCF54\uB4DC \uC5B8\uC5B4",
|
|
1370
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
1371
|
+
onChange: (e) => onCodeLangChange(e.target.value),
|
|
1372
|
+
children: CODE_LANGUAGES.map((l) => /* @__PURE__ */ jsx("option", { value: l.value, children: l.label }, l.value))
|
|
1373
|
+
}
|
|
1374
|
+
)
|
|
1375
|
+
] });
|
|
647
1376
|
};
|
|
648
1377
|
var SlashMenu = ({ position, items, activeIndex, onSelect, onClose }) => {
|
|
649
1378
|
React.useEffect(() => {
|