@x-plat/design-system 0.11.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/Dropdown/index.cjs +22 -9
- package/dist/components/Dropdown/index.css +3 -0
- package/dist/components/Dropdown/index.js +22 -9
- package/dist/components/Editor/index.cjs +602 -30
- 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 +602 -30
- package/dist/components/PopOver/index.cjs +21 -7
- package/dist/components/PopOver/index.css +3 -0
- package/dist/components/PopOver/index.js +21 -7
- package/dist/components/Select/index.cjs +21 -8
- package/dist/components/Select/index.css +3 -0
- package/dist/components/Select/index.js +21 -8
- package/dist/components/TimePicker/index.cjs +21 -8
- package/dist/components/TimePicker/index.css +3 -0
- package/dist/components/TimePicker/index.js +21 -8
- package/dist/components/index.cjs +629 -41
- package/dist/components/index.css +110 -0
- package/dist/components/index.js +629 -41
- package/dist/index.cjs +629 -41
- package/dist/index.css +110 -0
- package/dist/index.js +629 -41
- package/guidelines/components/editor.md +58 -3
- package/package.json +1 -1
|
@@ -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,11 +177,41 @@ 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
|
};
|
|
70
216
|
var DROP_WITH_CONTENT = /* @__PURE__ */ new Set([
|
|
71
217
|
"script",
|
|
@@ -118,7 +264,9 @@ var escapeHtml = (text) => {
|
|
|
118
264
|
};
|
|
119
265
|
var SLASH_ITEMS = [
|
|
120
266
|
{ key: "paragraph", label: "\uB2E8\uB77D", hint: "\uAE30\uBCF8 \uD14D\uC2A4\uD2B8" },
|
|
121
|
-
{ 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: "" },
|
|
122
270
|
{ key: "list", label: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D", hint: "\u2022 \uD56D\uBAA9" },
|
|
123
271
|
{ key: "ordered-list", label: "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D", hint: "1. \uD56D\uBAA9" },
|
|
124
272
|
{ key: "blockquote", label: "\uC778\uC6A9", hint: "" },
|
|
@@ -161,6 +309,8 @@ var Editor = (props) => {
|
|
|
161
309
|
el.innerHTML = sanitizeHtml(value || "");
|
|
162
310
|
if (!el.firstChild) el.innerHTML = "<p><br></p>";
|
|
163
311
|
lastEmittedRef.current = null;
|
|
312
|
+
historyRef.current = [{ html: el.innerHTML, caret: 0 }];
|
|
313
|
+
historyIdxRef.current = 0;
|
|
164
314
|
updateEmpty();
|
|
165
315
|
}, [value]);
|
|
166
316
|
const updateEmpty = () => {
|
|
@@ -205,6 +355,17 @@ var Editor = (props) => {
|
|
|
205
355
|
if (document.queryCommandState("insertOrderedList")) next.add("ordered-list");
|
|
206
356
|
const block = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
|
|
207
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");
|
|
208
369
|
} catch {
|
|
209
370
|
}
|
|
210
371
|
setActiveFormats(next);
|
|
@@ -218,6 +379,122 @@ var Editor = (props) => {
|
|
|
218
379
|
const setBlock = (tag) => {
|
|
219
380
|
exec("formatBlock", tag);
|
|
220
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
|
+
};
|
|
221
498
|
const closestPre = () => {
|
|
222
499
|
const sel = window.getSelection();
|
|
223
500
|
const node = sel?.anchorNode;
|
|
@@ -226,6 +503,7 @@ var Editor = (props) => {
|
|
|
226
503
|
return el?.closest("pre") ?? null;
|
|
227
504
|
};
|
|
228
505
|
const insertCodeBlock = () => {
|
|
506
|
+
pushHistory(true);
|
|
229
507
|
const el = editorRef.current;
|
|
230
508
|
const sel = window.getSelection();
|
|
231
509
|
if (!el || !sel || sel.rangeCount === 0) return;
|
|
@@ -308,7 +586,167 @@ var Editor = (props) => {
|
|
|
308
586
|
sel.addRange(r2);
|
|
309
587
|
editorRef.current?.focus();
|
|
310
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
|
+
};
|
|
311
748
|
const insertDivider = () => {
|
|
749
|
+
pushHistory(true);
|
|
312
750
|
exec("insertHorizontalRule");
|
|
313
751
|
const el = editorRef.current;
|
|
314
752
|
if (el && el.lastElementChild?.tagName === "HR") {
|
|
@@ -401,8 +839,14 @@ var Editor = (props) => {
|
|
|
401
839
|
case "paragraph":
|
|
402
840
|
setBlock("p");
|
|
403
841
|
break;
|
|
404
|
-
case "heading":
|
|
405
|
-
|
|
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");
|
|
406
850
|
break;
|
|
407
851
|
case "list":
|
|
408
852
|
exec("insertUnorderedList");
|
|
@@ -495,6 +939,8 @@ var Editor = (props) => {
|
|
|
495
939
|
if (!el) return;
|
|
496
940
|
const html = el.innerHTML;
|
|
497
941
|
lastEmittedRef.current = html;
|
|
942
|
+
pushHistory();
|
|
943
|
+
scheduleHighlight();
|
|
498
944
|
onChange?.(html);
|
|
499
945
|
updateEmpty();
|
|
500
946
|
updateActiveFormats();
|
|
@@ -548,6 +994,15 @@ var Editor = (props) => {
|
|
|
548
994
|
return;
|
|
549
995
|
}
|
|
550
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
|
+
}
|
|
551
1006
|
if (slashOpen) {
|
|
552
1007
|
if (e.key === "ArrowDown") {
|
|
553
1008
|
e.preventDefault();
|
|
@@ -571,6 +1026,21 @@ var Editor = (props) => {
|
|
|
571
1026
|
return;
|
|
572
1027
|
}
|
|
573
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
|
+
}
|
|
574
1044
|
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
|
|
575
1045
|
const k = e.key.toLowerCase();
|
|
576
1046
|
if (k === "b") {
|
|
@@ -604,10 +1074,16 @@ var Editor = (props) => {
|
|
|
604
1074
|
}
|
|
605
1075
|
}
|
|
606
1076
|
};
|
|
1077
|
+
const plainPasteRef = React.useRef(false);
|
|
607
1078
|
const handlePaste = (e) => {
|
|
608
1079
|
if (readOnly) return;
|
|
609
1080
|
e.preventDefault();
|
|
610
1081
|
const text = e.clipboardData.getData("text/plain");
|
|
1082
|
+
if (plainPasteRef.current) {
|
|
1083
|
+
plainPasteRef.current = false;
|
|
1084
|
+
insertTextAtCaret(text);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
611
1087
|
if (closestPre()) {
|
|
612
1088
|
insertTextAtCaret(text);
|
|
613
1089
|
return;
|
|
@@ -634,9 +1110,11 @@ var Editor = (props) => {
|
|
|
634
1110
|
};
|
|
635
1111
|
const handleSelectionUpdate = () => {
|
|
636
1112
|
updateActiveFormats();
|
|
1113
|
+
const pre = closestPre();
|
|
1114
|
+
setCodeLang(pre ? langOf(pre) : "plain");
|
|
637
1115
|
};
|
|
638
1116
|
const onToolbarAction = (item) => {
|
|
639
|
-
if (closestPre() &&
|
|
1117
|
+
if (closestPre() && !["code-block", "undo", "redo"].includes(item)) return;
|
|
640
1118
|
switch (item) {
|
|
641
1119
|
case "bold":
|
|
642
1120
|
exec("bold");
|
|
@@ -665,8 +1143,14 @@ var Editor = (props) => {
|
|
|
665
1143
|
handleInput();
|
|
666
1144
|
break;
|
|
667
1145
|
}
|
|
668
|
-
case "heading":
|
|
669
|
-
|
|
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");
|
|
670
1154
|
break;
|
|
671
1155
|
case "list":
|
|
672
1156
|
exec("insertUnorderedList");
|
|
@@ -689,6 +1173,39 @@ var Editor = (props) => {
|
|
|
689
1173
|
case "divider":
|
|
690
1174
|
insertDivider();
|
|
691
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;
|
|
692
1209
|
}
|
|
693
1210
|
};
|
|
694
1211
|
return /* @__PURE__ */ jsxs("div", { className: clsx_default("lib-xplat-editor", isFocused && "focused", readOnly && "readonly"), children: [
|
|
@@ -697,7 +1214,10 @@ var Editor = (props) => {
|
|
|
697
1214
|
{
|
|
698
1215
|
items: toolbar,
|
|
699
1216
|
active: activeFormats,
|
|
700
|
-
onAction: onToolbarAction
|
|
1217
|
+
onAction: onToolbarAction,
|
|
1218
|
+
codeLang,
|
|
1219
|
+
inCodeBlock: activeFormats.has("block:pre"),
|
|
1220
|
+
onCodeLangChange: changeCodeLang
|
|
701
1221
|
}
|
|
702
1222
|
),
|
|
703
1223
|
/* @__PURE__ */ jsxs("div", { className: "lib-xplat-editor__content", style: { minHeight }, children: [
|
|
@@ -715,6 +1235,7 @@ var Editor = (props) => {
|
|
|
715
1235
|
className: "lib-xplat-editor__editable",
|
|
716
1236
|
onInput: handleInput,
|
|
717
1237
|
onKeyDown: handleKeyDown,
|
|
1238
|
+
onClick: handleEditorClick,
|
|
718
1239
|
onKeyUp: handleSelectionUpdate,
|
|
719
1240
|
onMouseUp: handleSelectionUpdate,
|
|
720
1241
|
onPaste: handlePaste,
|
|
@@ -756,14 +1277,27 @@ var TOOLBAR_LABEL = {
|
|
|
756
1277
|
underline: "U",
|
|
757
1278
|
strikethrough: "S",
|
|
758
1279
|
code: "<>",
|
|
759
|
-
heading: "
|
|
1280
|
+
"heading-1": "H1",
|
|
1281
|
+
"heading-2": "H2",
|
|
1282
|
+
"heading-3": "H3",
|
|
760
1283
|
list: "\u2022",
|
|
761
1284
|
"ordered-list": "1.",
|
|
762
1285
|
blockquote: "\u275D",
|
|
763
1286
|
"code-block": "[ ]",
|
|
764
1287
|
link: "\u{1F517}",
|
|
765
1288
|
image: "\u{1F5BC}",
|
|
766
|
-
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"
|
|
767
1301
|
};
|
|
768
1302
|
var TOOLBAR_TITLE = {
|
|
769
1303
|
bold: "\uAD75\uAC8C (\u2318B)",
|
|
@@ -771,36 +1305,74 @@ var TOOLBAR_TITLE = {
|
|
|
771
1305
|
underline: "\uBC11\uC904 (\u2318U)",
|
|
772
1306
|
strikethrough: "\uCDE8\uC18C\uC120",
|
|
773
1307
|
code: "\uC778\uB77C\uC778 \uCF54\uB4DC",
|
|
774
|
-
heading: "\uC81C\uBAA9",
|
|
1308
|
+
"heading-1": "\uC81C\uBAA9 1",
|
|
1309
|
+
"heading-2": "\uC81C\uBAA9 2",
|
|
1310
|
+
"heading-3": "\uC81C\uBAA9 3",
|
|
775
1311
|
list: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D",
|
|
776
1312
|
"ordered-list": "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D",
|
|
777
1313
|
blockquote: "\uC778\uC6A9",
|
|
778
1314
|
"code-block": "\uCF54\uB4DC \uBE14\uB85D",
|
|
779
1315
|
link: "\uB9C1\uD06C (\u2318K)",
|
|
780
1316
|
image: "\uC774\uBBF8\uC9C0",
|
|
781
|
-
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)"
|
|
782
1329
|
};
|
|
783
1330
|
var isActive = (item, active) => {
|
|
784
|
-
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");
|
|
785
1334
|
if (item === "blockquote") return active.has("block:blockquote");
|
|
786
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");
|
|
787
1339
|
return active.has(item);
|
|
788
1340
|
};
|
|
789
|
-
var EditorToolbar = ({
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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]
|
|
799
1361
|
},
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
+
] });
|
|
804
1376
|
};
|
|
805
1377
|
var SlashMenu = ({ position, items, activeIndex, onSelect, onClose }) => {
|
|
806
1378
|
React.useEffect(() => {
|