@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
|
@@ -53,6 +53,109 @@ function clsx() {
|
|
|
53
53
|
}
|
|
54
54
|
var clsx_default = clsx;
|
|
55
55
|
|
|
56
|
+
// src/components/Editor/highlight.ts
|
|
57
|
+
var CODE_LANGUAGES = [
|
|
58
|
+
{ value: "plain", label: "\uC5C6\uC74C" },
|
|
59
|
+
{ value: "javascript", label: "JavaScript" },
|
|
60
|
+
{ value: "typescript", label: "TypeScript" },
|
|
61
|
+
{ value: "json", label: "JSON" },
|
|
62
|
+
{ value: "html", label: "HTML" },
|
|
63
|
+
{ value: "css", label: "CSS" },
|
|
64
|
+
{ value: "python", label: "Python" },
|
|
65
|
+
{ value: "sql", label: "SQL" },
|
|
66
|
+
{ value: "bash", label: "Bash" }
|
|
67
|
+
];
|
|
68
|
+
var KEYWORDS = {
|
|
69
|
+
plain: [],
|
|
70
|
+
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(" "),
|
|
71
|
+
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(" "),
|
|
72
|
+
json: ["true", "false", "null"],
|
|
73
|
+
html: [],
|
|
74
|
+
css: [],
|
|
75
|
+
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(" "),
|
|
76
|
+
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(" "),
|
|
77
|
+
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(" ")
|
|
78
|
+
};
|
|
79
|
+
var LINE_COMMENT = {
|
|
80
|
+
javascript: "//",
|
|
81
|
+
typescript: "//",
|
|
82
|
+
css: "",
|
|
83
|
+
python: "#",
|
|
84
|
+
sql: "--",
|
|
85
|
+
bash: "#"
|
|
86
|
+
};
|
|
87
|
+
var escape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
88
|
+
var wrap = (cls, text) => `<span class="tok-${cls}">${escape(text)}</span>`;
|
|
89
|
+
var highlight = (code, lang) => {
|
|
90
|
+
if (lang === "plain" || !code) return escape(code);
|
|
91
|
+
const keywords = new Set(KEYWORDS[lang]);
|
|
92
|
+
const lineComment = LINE_COMMENT[lang];
|
|
93
|
+
const caseInsensitive = lang === "sql";
|
|
94
|
+
let out = "";
|
|
95
|
+
let i = 0;
|
|
96
|
+
while (i < code.length) {
|
|
97
|
+
const rest = code.slice(i);
|
|
98
|
+
if ((lang === "javascript" || lang === "typescript" || lang === "css") && rest.startsWith("/*")) {
|
|
99
|
+
const end = code.indexOf("*/", i + 2);
|
|
100
|
+
const stop = end === -1 ? code.length : end + 2;
|
|
101
|
+
out += wrap("comment", code.slice(i, stop));
|
|
102
|
+
i = stop;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (lineComment && rest.startsWith(lineComment)) {
|
|
106
|
+
const nl = code.indexOf("\n", i);
|
|
107
|
+
const stop = nl === -1 ? code.length : nl;
|
|
108
|
+
out += wrap("comment", code.slice(i, stop));
|
|
109
|
+
i = stop;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const q = rest[0];
|
|
113
|
+
if (q === '"' || q === "'" || q === "`") {
|
|
114
|
+
let j = 1;
|
|
115
|
+
while (j < rest.length) {
|
|
116
|
+
if (rest[j] === "\\") {
|
|
117
|
+
j += 2;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (rest[j] === q) {
|
|
121
|
+
j += 1;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
if (rest[j] === "\n" && q !== "`") break;
|
|
125
|
+
j += 1;
|
|
126
|
+
}
|
|
127
|
+
out += wrap("string", rest.slice(0, j));
|
|
128
|
+
i += j;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const num = /^\d+(\.\d+)?/.exec(rest);
|
|
132
|
+
if (num && !/[\w$]/.test(code[i - 1] ?? "")) {
|
|
133
|
+
out += wrap("number", num[0]);
|
|
134
|
+
i += num[0].length;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const word = /^[A-Za-z_$][\w$]*/.exec(rest);
|
|
138
|
+
if (word) {
|
|
139
|
+
const w = word[0];
|
|
140
|
+
const hit = caseInsensitive ? keywords.has(w.toUpperCase()) : keywords.has(w);
|
|
141
|
+
out += hit ? wrap("keyword", w) : escape(w);
|
|
142
|
+
i += w.length;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (lang === "html" && rest.startsWith("<")) {
|
|
146
|
+
const tag = /^<\/?[A-Za-z][\w-]*/.exec(rest);
|
|
147
|
+
if (tag) {
|
|
148
|
+
out += wrap("keyword", tag[0]);
|
|
149
|
+
i += tag[0].length;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
out += escape(code[i]);
|
|
154
|
+
i += 1;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
};
|
|
158
|
+
|
|
56
159
|
// src/components/Editor/Editor.tsx
|
|
57
160
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
58
161
|
var DEFAULT_TOOLBAR = [
|
|
@@ -61,14 +164,27 @@ var DEFAULT_TOOLBAR = [
|
|
|
61
164
|
"underline",
|
|
62
165
|
"strikethrough",
|
|
63
166
|
"code",
|
|
64
|
-
"heading",
|
|
167
|
+
"heading-1",
|
|
168
|
+
"heading-2",
|
|
169
|
+
"heading-3",
|
|
65
170
|
"list",
|
|
66
171
|
"ordered-list",
|
|
67
172
|
"blockquote",
|
|
68
173
|
"code-block",
|
|
69
174
|
"link",
|
|
70
175
|
"image",
|
|
71
|
-
"divider"
|
|
176
|
+
"divider",
|
|
177
|
+
"checklist",
|
|
178
|
+
"table",
|
|
179
|
+
"align-left",
|
|
180
|
+
"align-center",
|
|
181
|
+
"align-right",
|
|
182
|
+
"highlight",
|
|
183
|
+
"superscript",
|
|
184
|
+
"subscript",
|
|
185
|
+
"clear-format",
|
|
186
|
+
"undo",
|
|
187
|
+
"redo"
|
|
72
188
|
];
|
|
73
189
|
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
74
190
|
"p",
|
|
@@ -97,11 +213,41 @@ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
|
97
213
|
"img",
|
|
98
214
|
"hr",
|
|
99
215
|
"span",
|
|
100
|
-
"div"
|
|
216
|
+
"div",
|
|
217
|
+
"sub",
|
|
218
|
+
"sup",
|
|
219
|
+
"mark",
|
|
220
|
+
"table",
|
|
221
|
+
"thead",
|
|
222
|
+
"tbody",
|
|
223
|
+
"tr",
|
|
224
|
+
"th",
|
|
225
|
+
"td"
|
|
101
226
|
]);
|
|
102
227
|
var ALLOWED_ATTRS = {
|
|
103
228
|
a: ["href", "target", "rel"],
|
|
104
|
-
img: ["src", "alt"]
|
|
229
|
+
img: ["src", "alt", "width"],
|
|
230
|
+
// 정렬·체크리스트는 클래스로만 표현한다. style 을 열면 붙여넣기로 아무 CSS 나 들어온다
|
|
231
|
+
p: ["class"],
|
|
232
|
+
h1: ["class"],
|
|
233
|
+
h2: ["class"],
|
|
234
|
+
h3: ["class"],
|
|
235
|
+
h4: ["class"],
|
|
236
|
+
h5: ["class"],
|
|
237
|
+
h6: ["class"],
|
|
238
|
+
ul: ["class"],
|
|
239
|
+
ol: ["class"],
|
|
240
|
+
li: ["class", "data-checked"],
|
|
241
|
+
blockquote: ["class"],
|
|
242
|
+
div: ["class"],
|
|
243
|
+
span: ["class"],
|
|
244
|
+
pre: ["class"],
|
|
245
|
+
code: ["class"],
|
|
246
|
+
mark: ["class"],
|
|
247
|
+
table: ["class"],
|
|
248
|
+
td: ["class"],
|
|
249
|
+
th: ["class"],
|
|
250
|
+
tr: ["class"]
|
|
105
251
|
};
|
|
106
252
|
var DROP_WITH_CONTENT = /* @__PURE__ */ new Set([
|
|
107
253
|
"script",
|
|
@@ -154,7 +300,9 @@ var escapeHtml = (text) => {
|
|
|
154
300
|
};
|
|
155
301
|
var SLASH_ITEMS = [
|
|
156
302
|
{ key: "paragraph", label: "\uB2E8\uB77D", hint: "\uAE30\uBCF8 \uD14D\uC2A4\uD2B8" },
|
|
157
|
-
{ key: "heading", label: "\uC81C\uBAA9 1", hint: "\uD070 \uC81C\uBAA9" },
|
|
303
|
+
{ key: "heading-1", label: "\uC81C\uBAA9 1", hint: "\uAC00\uC7A5 \uD070 \uC81C\uBAA9" },
|
|
304
|
+
{ key: "heading-2", label: "\uC81C\uBAA9 2", hint: "" },
|
|
305
|
+
{ key: "heading-3", label: "\uC81C\uBAA9 3", hint: "" },
|
|
158
306
|
{ key: "list", label: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D", hint: "\u2022 \uD56D\uBAA9" },
|
|
159
307
|
{ key: "ordered-list", label: "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D", hint: "1. \uD56D\uBAA9" },
|
|
160
308
|
{ key: "blockquote", label: "\uC778\uC6A9", hint: "" },
|
|
@@ -197,6 +345,8 @@ var Editor = (props) => {
|
|
|
197
345
|
el.innerHTML = sanitizeHtml(value || "");
|
|
198
346
|
if (!el.firstChild) el.innerHTML = "<p><br></p>";
|
|
199
347
|
lastEmittedRef.current = null;
|
|
348
|
+
historyRef.current = [{ html: el.innerHTML, caret: 0 }];
|
|
349
|
+
historyIdxRef.current = 0;
|
|
200
350
|
updateEmpty();
|
|
201
351
|
}, [value]);
|
|
202
352
|
const updateEmpty = () => {
|
|
@@ -241,6 +391,17 @@ var Editor = (props) => {
|
|
|
241
391
|
if (document.queryCommandState("insertOrderedList")) next.add("ordered-list");
|
|
242
392
|
const block = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
|
|
243
393
|
if (block) next.add(`block:${block}`);
|
|
394
|
+
if (document.queryCommandState("superscript")) next.add("superscript");
|
|
395
|
+
if (document.queryCommandState("subscript")) next.add("subscript");
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const blockEl = currentBlock();
|
|
400
|
+
if (blockEl?.classList.contains("align-center")) next.add("align:center");
|
|
401
|
+
else if (blockEl?.classList.contains("align-right")) next.add("align:right");
|
|
402
|
+
else if (blockEl) next.add("align:left");
|
|
403
|
+
if (blockEl?.closest("mark")) next.add("highlight");
|
|
404
|
+
if (blockEl?.closest("ul.checklist")) next.add("checklist");
|
|
244
405
|
} catch {
|
|
245
406
|
}
|
|
246
407
|
setActiveFormats(next);
|
|
@@ -254,6 +415,122 @@ var Editor = (props) => {
|
|
|
254
415
|
const setBlock = (tag) => {
|
|
255
416
|
exec("formatBlock", tag);
|
|
256
417
|
};
|
|
418
|
+
const toggleBlock = (tag) => {
|
|
419
|
+
const current = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
|
|
420
|
+
setBlock(current === tag ? "p" : tag);
|
|
421
|
+
};
|
|
422
|
+
const historyRef = import_react.default.useRef([]);
|
|
423
|
+
const historyIdxRef = import_react.default.useRef(-1);
|
|
424
|
+
const lastPushAtRef = import_react.default.useRef(0);
|
|
425
|
+
const isRestoringRef = import_react.default.useRef(false);
|
|
426
|
+
const caretOffset = (rootEl) => {
|
|
427
|
+
const el = rootEl ?? editorRef.current;
|
|
428
|
+
const sel = window.getSelection();
|
|
429
|
+
if (!el || !sel || sel.rangeCount === 0 || !el.contains(sel.anchorNode)) return 0;
|
|
430
|
+
const range = sel.getRangeAt(0).cloneRange();
|
|
431
|
+
range.selectNodeContents(el);
|
|
432
|
+
range.setEnd(sel.getRangeAt(0).endContainer, sel.getRangeAt(0).endOffset);
|
|
433
|
+
return range.toString().length;
|
|
434
|
+
};
|
|
435
|
+
const setCaretOffset = (offset, rootEl) => {
|
|
436
|
+
const el = rootEl ?? editorRef.current;
|
|
437
|
+
if (!el) return;
|
|
438
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
439
|
+
let remaining = offset;
|
|
440
|
+
let node = null;
|
|
441
|
+
while (node = walker.nextNode()) {
|
|
442
|
+
const len = node.textContent?.length ?? 0;
|
|
443
|
+
if (remaining <= len) {
|
|
444
|
+
const range = document.createRange();
|
|
445
|
+
range.setStart(node, remaining);
|
|
446
|
+
range.collapse(true);
|
|
447
|
+
const sel = window.getSelection();
|
|
448
|
+
sel?.removeAllRanges();
|
|
449
|
+
sel?.addRange(range);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
remaining -= len;
|
|
453
|
+
}
|
|
454
|
+
placeCaretAtEnd(el);
|
|
455
|
+
};
|
|
456
|
+
const pushHistory = (force = false) => {
|
|
457
|
+
const el = editorRef.current;
|
|
458
|
+
if (!el || isRestoringRef.current) return;
|
|
459
|
+
const html = el.innerHTML;
|
|
460
|
+
const hist = historyRef.current;
|
|
461
|
+
const cur = hist[historyIdxRef.current];
|
|
462
|
+
if (cur && cur.html === html) return;
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
const coalesce = !force && cur && now - lastPushAtRef.current < 500;
|
|
465
|
+
lastPushAtRef.current = now;
|
|
466
|
+
if (coalesce) {
|
|
467
|
+
hist[historyIdxRef.current] = { html, caret: caretOffset() };
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
hist.length = historyIdxRef.current + 1;
|
|
471
|
+
hist.push({ html, caret: caretOffset() });
|
|
472
|
+
if (hist.length > 200) hist.shift();
|
|
473
|
+
historyIdxRef.current = hist.length - 1;
|
|
474
|
+
};
|
|
475
|
+
const applySnapshot = (snap) => {
|
|
476
|
+
const el = editorRef.current;
|
|
477
|
+
if (!el) return;
|
|
478
|
+
isRestoringRef.current = true;
|
|
479
|
+
el.innerHTML = snap.html;
|
|
480
|
+
setCaretOffset(snap.caret);
|
|
481
|
+
isRestoringRef.current = false;
|
|
482
|
+
lastEmittedRef.current = el.innerHTML;
|
|
483
|
+
onChange?.(el.innerHTML);
|
|
484
|
+
updateEmpty();
|
|
485
|
+
updateActiveFormats();
|
|
486
|
+
};
|
|
487
|
+
const undo = () => {
|
|
488
|
+
pushHistory(true);
|
|
489
|
+
if (historyIdxRef.current <= 0) return;
|
|
490
|
+
historyIdxRef.current -= 1;
|
|
491
|
+
applySnapshot(historyRef.current[historyIdxRef.current]);
|
|
492
|
+
};
|
|
493
|
+
const redo = () => {
|
|
494
|
+
if (historyIdxRef.current >= historyRef.current.length - 1) return;
|
|
495
|
+
historyIdxRef.current += 1;
|
|
496
|
+
applySnapshot(historyRef.current[historyIdxRef.current]);
|
|
497
|
+
};
|
|
498
|
+
const langOf = (pre) => {
|
|
499
|
+
const cls = Array.from(pre.classList).find((c) => c.startsWith("lang-"));
|
|
500
|
+
return cls?.slice(5) ?? "plain";
|
|
501
|
+
};
|
|
502
|
+
const [codeLang, setCodeLang] = import_react.default.useState("plain");
|
|
503
|
+
const highlightRafRef = import_react.default.useRef(0);
|
|
504
|
+
const rehighlight = (pre) => {
|
|
505
|
+
if (composingRef.current) return;
|
|
506
|
+
const code = pre.querySelector("code");
|
|
507
|
+
if (!code) return;
|
|
508
|
+
const lang = langOf(pre);
|
|
509
|
+
const text = code.textContent || "";
|
|
510
|
+
const next = highlight(text, lang);
|
|
511
|
+
if (code.innerHTML === next) return;
|
|
512
|
+
const caret = caretOffset(code);
|
|
513
|
+
isRestoringRef.current = true;
|
|
514
|
+
code.innerHTML = next;
|
|
515
|
+
setCaretOffset(caret, code);
|
|
516
|
+
isRestoringRef.current = false;
|
|
517
|
+
};
|
|
518
|
+
const scheduleHighlight = () => {
|
|
519
|
+
const pre = closestPre();
|
|
520
|
+
if (!pre) return;
|
|
521
|
+
cancelAnimationFrame(highlightRafRef.current);
|
|
522
|
+
highlightRafRef.current = requestAnimationFrame(() => rehighlight(pre));
|
|
523
|
+
};
|
|
524
|
+
const changeCodeLang = (lang) => {
|
|
525
|
+
const pre = closestPre();
|
|
526
|
+
if (!pre) return;
|
|
527
|
+
pushHistory(true);
|
|
528
|
+
Array.from(pre.classList).filter((c) => c.startsWith("lang-")).forEach((c) => pre.classList.remove(c));
|
|
529
|
+
if (lang !== "plain") pre.classList.add(`lang-${lang}`);
|
|
530
|
+
setCodeLang(lang);
|
|
531
|
+
rehighlight(pre);
|
|
532
|
+
handleInput();
|
|
533
|
+
};
|
|
257
534
|
const closestPre = () => {
|
|
258
535
|
const sel = window.getSelection();
|
|
259
536
|
const node = sel?.anchorNode;
|
|
@@ -262,6 +539,7 @@ var Editor = (props) => {
|
|
|
262
539
|
return el?.closest("pre") ?? null;
|
|
263
540
|
};
|
|
264
541
|
const insertCodeBlock = () => {
|
|
542
|
+
pushHistory(true);
|
|
265
543
|
const el = editorRef.current;
|
|
266
544
|
const sel = window.getSelection();
|
|
267
545
|
if (!el || !sel || sel.rangeCount === 0) return;
|
|
@@ -344,7 +622,167 @@ var Editor = (props) => {
|
|
|
344
622
|
sel.addRange(r2);
|
|
345
623
|
editorRef.current?.focus();
|
|
346
624
|
};
|
|
625
|
+
const clearFormat = () => {
|
|
626
|
+
exec("removeFormat");
|
|
627
|
+
exec("unlink");
|
|
628
|
+
setBlock("p");
|
|
629
|
+
};
|
|
630
|
+
const indentList = (outdent) => {
|
|
631
|
+
exec(outdent ? "outdent" : "indent");
|
|
632
|
+
};
|
|
633
|
+
const inListItem = () => {
|
|
634
|
+
const sel = window.getSelection();
|
|
635
|
+
const node = sel?.anchorNode;
|
|
636
|
+
if (!node || !editorRef.current?.contains(node)) return false;
|
|
637
|
+
const el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
638
|
+
return !!el?.closest("li");
|
|
639
|
+
};
|
|
640
|
+
const currentBlock = () => {
|
|
641
|
+
const sel = window.getSelection();
|
|
642
|
+
const node = sel?.anchorNode;
|
|
643
|
+
const root = editorRef.current;
|
|
644
|
+
if (!node || !root?.contains(node)) return null;
|
|
645
|
+
let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
646
|
+
while (el && el !== root && !/^(P|H[1-6]|LI|BLOCKQUOTE|PRE|DIV|TD|TH)$/.test(el.tagName)) {
|
|
647
|
+
el = el.parentElement;
|
|
648
|
+
}
|
|
649
|
+
return el && el !== root ? el : null;
|
|
650
|
+
};
|
|
651
|
+
const setAlign = (align) => {
|
|
652
|
+
const block = currentBlock();
|
|
653
|
+
if (!block) return;
|
|
654
|
+
pushHistory(true);
|
|
655
|
+
block.classList.remove("align-left", "align-center", "align-right");
|
|
656
|
+
if (align !== "left") block.classList.add(`align-${align}`);
|
|
657
|
+
handleInput();
|
|
658
|
+
};
|
|
659
|
+
const toggleChecklist = () => {
|
|
660
|
+
pushHistory(true);
|
|
661
|
+
const block = currentBlock();
|
|
662
|
+
const list = block?.closest("ul");
|
|
663
|
+
if (list?.classList.contains("checklist")) {
|
|
664
|
+
list.classList.remove("checklist");
|
|
665
|
+
list.querySelectorAll("li").forEach((li) => li.removeAttribute("data-checked"));
|
|
666
|
+
handleInput();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (list) {
|
|
670
|
+
list.classList.add("checklist");
|
|
671
|
+
list.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
|
|
672
|
+
handleInput();
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
exec("insertUnorderedList");
|
|
676
|
+
const made = currentBlock()?.closest("ul");
|
|
677
|
+
if (made) {
|
|
678
|
+
made.classList.add("checklist");
|
|
679
|
+
made.querySelectorAll("li").forEach((li) => li.setAttribute("data-checked", "false"));
|
|
680
|
+
}
|
|
681
|
+
handleInput();
|
|
682
|
+
};
|
|
683
|
+
const handleEditorClick = (e) => {
|
|
684
|
+
if (readOnly) return;
|
|
685
|
+
const target = e.target;
|
|
686
|
+
const li = target.closest?.("li");
|
|
687
|
+
if (!li || !li.closest("ul.checklist")) return;
|
|
688
|
+
const rect = li.getBoundingClientRect();
|
|
689
|
+
if (e.clientX - rect.left > 20) return;
|
|
690
|
+
e.preventDefault();
|
|
691
|
+
pushHistory(true);
|
|
692
|
+
li.setAttribute("data-checked", li.getAttribute("data-checked") === "true" ? "false" : "true");
|
|
693
|
+
handleInput();
|
|
694
|
+
};
|
|
695
|
+
const toggleHighlight = () => {
|
|
696
|
+
const sel = window.getSelection();
|
|
697
|
+
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return;
|
|
698
|
+
pushHistory(true);
|
|
699
|
+
const range = sel.getRangeAt(0);
|
|
700
|
+
const node = sel.anchorNode;
|
|
701
|
+
const el = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
|
|
702
|
+
const existing = el?.closest("mark");
|
|
703
|
+
if (existing) {
|
|
704
|
+
const parent = existing.parentNode;
|
|
705
|
+
while (existing.firstChild) parent?.insertBefore(existing.firstChild, existing);
|
|
706
|
+
parent?.removeChild(existing);
|
|
707
|
+
handleInput();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const mark = document.createElement("mark");
|
|
711
|
+
try {
|
|
712
|
+
range.surroundContents(mark);
|
|
713
|
+
} catch {
|
|
714
|
+
const frag = range.extractContents();
|
|
715
|
+
mark.appendChild(frag);
|
|
716
|
+
range.insertNode(mark);
|
|
717
|
+
}
|
|
718
|
+
handleInput();
|
|
719
|
+
};
|
|
720
|
+
const insertTable = (cols = 3, rows = 2) => {
|
|
721
|
+
const sel = window.getSelection();
|
|
722
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
723
|
+
pushHistory(true);
|
|
724
|
+
const table = document.createElement("table");
|
|
725
|
+
const thead = document.createElement("thead");
|
|
726
|
+
const headRow = document.createElement("tr");
|
|
727
|
+
for (let c = 0; c < cols; c++) {
|
|
728
|
+
const th = document.createElement("th");
|
|
729
|
+
th.innerHTML = "<br>";
|
|
730
|
+
headRow.appendChild(th);
|
|
731
|
+
}
|
|
732
|
+
thead.appendChild(headRow);
|
|
733
|
+
table.appendChild(thead);
|
|
734
|
+
const tbody = document.createElement("tbody");
|
|
735
|
+
for (let r2 = 0; r2 < rows; r2++) {
|
|
736
|
+
const tr = document.createElement("tr");
|
|
737
|
+
for (let c = 0; c < cols; c++) {
|
|
738
|
+
const td = document.createElement("td");
|
|
739
|
+
td.innerHTML = "<br>";
|
|
740
|
+
tr.appendChild(td);
|
|
741
|
+
}
|
|
742
|
+
tbody.appendChild(tr);
|
|
743
|
+
}
|
|
744
|
+
table.appendChild(tbody);
|
|
745
|
+
const range = sel.getRangeAt(0);
|
|
746
|
+
range.deleteContents();
|
|
747
|
+
range.insertNode(table);
|
|
748
|
+
ensureParagraphAfter(table);
|
|
749
|
+
const first = table.querySelector("th");
|
|
750
|
+
if (first) placeCaretAtEnd(first);
|
|
751
|
+
handleInput();
|
|
752
|
+
};
|
|
753
|
+
const moveTableCell = (backwards) => {
|
|
754
|
+
const block = currentBlock();
|
|
755
|
+
const cell = block?.closest("td, th");
|
|
756
|
+
if (!cell) return false;
|
|
757
|
+
const table = cell.closest("table");
|
|
758
|
+
if (!table) return false;
|
|
759
|
+
const cells = Array.from(table.querySelectorAll("td, th"));
|
|
760
|
+
const i = cells.indexOf(cell);
|
|
761
|
+
const next = cells[i + (backwards ? -1 : 1)];
|
|
762
|
+
if (next) {
|
|
763
|
+
placeCaretAtEnd(next);
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
if (!backwards) {
|
|
767
|
+
pushHistory(true);
|
|
768
|
+
const lastRow = table.querySelector("tbody")?.lastElementChild;
|
|
769
|
+
if (lastRow) {
|
|
770
|
+
const tr = document.createElement("tr");
|
|
771
|
+
for (let c = 0; c < lastRow.children.length; c++) {
|
|
772
|
+
const td = document.createElement("td");
|
|
773
|
+
td.innerHTML = "<br>";
|
|
774
|
+
tr.appendChild(td);
|
|
775
|
+
}
|
|
776
|
+
lastRow.after(tr);
|
|
777
|
+
placeCaretAtEnd(tr.firstElementChild);
|
|
778
|
+
handleInput();
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return false;
|
|
783
|
+
};
|
|
347
784
|
const insertDivider = () => {
|
|
785
|
+
pushHistory(true);
|
|
348
786
|
exec("insertHorizontalRule");
|
|
349
787
|
const el = editorRef.current;
|
|
350
788
|
if (el && el.lastElementChild?.tagName === "HR") {
|
|
@@ -437,8 +875,14 @@ var Editor = (props) => {
|
|
|
437
875
|
case "paragraph":
|
|
438
876
|
setBlock("p");
|
|
439
877
|
break;
|
|
440
|
-
case "heading":
|
|
441
|
-
|
|
878
|
+
case "heading-1":
|
|
879
|
+
toggleBlock("h1");
|
|
880
|
+
break;
|
|
881
|
+
case "heading-2":
|
|
882
|
+
toggleBlock("h2");
|
|
883
|
+
break;
|
|
884
|
+
case "heading-3":
|
|
885
|
+
toggleBlock("h3");
|
|
442
886
|
break;
|
|
443
887
|
case "list":
|
|
444
888
|
exec("insertUnorderedList");
|
|
@@ -531,6 +975,8 @@ var Editor = (props) => {
|
|
|
531
975
|
if (!el) return;
|
|
532
976
|
const html = el.innerHTML;
|
|
533
977
|
lastEmittedRef.current = html;
|
|
978
|
+
pushHistory();
|
|
979
|
+
scheduleHighlight();
|
|
534
980
|
onChange?.(html);
|
|
535
981
|
updateEmpty();
|
|
536
982
|
updateActiveFormats();
|
|
@@ -584,6 +1030,15 @@ var Editor = (props) => {
|
|
|
584
1030
|
return;
|
|
585
1031
|
}
|
|
586
1032
|
}
|
|
1033
|
+
if (e.key === "Tab" && moveTableCell(e.shiftKey)) {
|
|
1034
|
+
e.preventDefault();
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (e.key === "Tab" && inListItem()) {
|
|
1038
|
+
e.preventDefault();
|
|
1039
|
+
indentList(e.shiftKey);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
587
1042
|
if (slashOpen) {
|
|
588
1043
|
if (e.key === "ArrowDown") {
|
|
589
1044
|
e.preventDefault();
|
|
@@ -607,6 +1062,21 @@ var Editor = (props) => {
|
|
|
607
1062
|
return;
|
|
608
1063
|
}
|
|
609
1064
|
}
|
|
1065
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z") {
|
|
1066
|
+
e.preventDefault();
|
|
1067
|
+
if (e.shiftKey) redo();
|
|
1068
|
+
else undo();
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "v") {
|
|
1072
|
+
plainPasteRef.current = true;
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "y") {
|
|
1076
|
+
e.preventDefault();
|
|
1077
|
+
redo();
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
610
1080
|
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
|
|
611
1081
|
const k = e.key.toLowerCase();
|
|
612
1082
|
if (k === "b") {
|
|
@@ -640,10 +1110,16 @@ var Editor = (props) => {
|
|
|
640
1110
|
}
|
|
641
1111
|
}
|
|
642
1112
|
};
|
|
1113
|
+
const plainPasteRef = import_react.default.useRef(false);
|
|
643
1114
|
const handlePaste = (e) => {
|
|
644
1115
|
if (readOnly) return;
|
|
645
1116
|
e.preventDefault();
|
|
646
1117
|
const text = e.clipboardData.getData("text/plain");
|
|
1118
|
+
if (plainPasteRef.current) {
|
|
1119
|
+
plainPasteRef.current = false;
|
|
1120
|
+
insertTextAtCaret(text);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
647
1123
|
if (closestPre()) {
|
|
648
1124
|
insertTextAtCaret(text);
|
|
649
1125
|
return;
|
|
@@ -670,9 +1146,11 @@ var Editor = (props) => {
|
|
|
670
1146
|
};
|
|
671
1147
|
const handleSelectionUpdate = () => {
|
|
672
1148
|
updateActiveFormats();
|
|
1149
|
+
const pre = closestPre();
|
|
1150
|
+
setCodeLang(pre ? langOf(pre) : "plain");
|
|
673
1151
|
};
|
|
674
1152
|
const onToolbarAction = (item) => {
|
|
675
|
-
if (closestPre() &&
|
|
1153
|
+
if (closestPre() && !["code-block", "undo", "redo"].includes(item)) return;
|
|
676
1154
|
switch (item) {
|
|
677
1155
|
case "bold":
|
|
678
1156
|
exec("bold");
|
|
@@ -701,8 +1179,14 @@ var Editor = (props) => {
|
|
|
701
1179
|
handleInput();
|
|
702
1180
|
break;
|
|
703
1181
|
}
|
|
704
|
-
case "heading":
|
|
705
|
-
|
|
1182
|
+
case "heading-1":
|
|
1183
|
+
toggleBlock("h1");
|
|
1184
|
+
break;
|
|
1185
|
+
case "heading-2":
|
|
1186
|
+
toggleBlock("h2");
|
|
1187
|
+
break;
|
|
1188
|
+
case "heading-3":
|
|
1189
|
+
toggleBlock("h3");
|
|
706
1190
|
break;
|
|
707
1191
|
case "list":
|
|
708
1192
|
exec("insertUnorderedList");
|
|
@@ -725,6 +1209,39 @@ var Editor = (props) => {
|
|
|
725
1209
|
case "divider":
|
|
726
1210
|
insertDivider();
|
|
727
1211
|
break;
|
|
1212
|
+
case "checklist":
|
|
1213
|
+
toggleChecklist();
|
|
1214
|
+
break;
|
|
1215
|
+
case "align-left":
|
|
1216
|
+
setAlign("left");
|
|
1217
|
+
break;
|
|
1218
|
+
case "align-center":
|
|
1219
|
+
setAlign("center");
|
|
1220
|
+
break;
|
|
1221
|
+
case "align-right":
|
|
1222
|
+
setAlign("right");
|
|
1223
|
+
break;
|
|
1224
|
+
case "highlight":
|
|
1225
|
+
toggleHighlight();
|
|
1226
|
+
break;
|
|
1227
|
+
case "superscript":
|
|
1228
|
+
exec("superscript");
|
|
1229
|
+
break;
|
|
1230
|
+
case "subscript":
|
|
1231
|
+
exec("subscript");
|
|
1232
|
+
break;
|
|
1233
|
+
case "table":
|
|
1234
|
+
insertTable();
|
|
1235
|
+
break;
|
|
1236
|
+
case "clear-format":
|
|
1237
|
+
clearFormat();
|
|
1238
|
+
break;
|
|
1239
|
+
case "undo":
|
|
1240
|
+
undo();
|
|
1241
|
+
break;
|
|
1242
|
+
case "redo":
|
|
1243
|
+
redo();
|
|
1244
|
+
break;
|
|
728
1245
|
}
|
|
729
1246
|
};
|
|
730
1247
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: clsx_default("lib-xplat-editor", isFocused && "focused", readOnly && "readonly"), children: [
|
|
@@ -733,7 +1250,10 @@ var Editor = (props) => {
|
|
|
733
1250
|
{
|
|
734
1251
|
items: toolbar,
|
|
735
1252
|
active: activeFormats,
|
|
736
|
-
onAction: onToolbarAction
|
|
1253
|
+
onAction: onToolbarAction,
|
|
1254
|
+
codeLang,
|
|
1255
|
+
inCodeBlock: activeFormats.has("block:pre"),
|
|
1256
|
+
onCodeLangChange: changeCodeLang
|
|
737
1257
|
}
|
|
738
1258
|
),
|
|
739
1259
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "lib-xplat-editor__content", style: { minHeight }, children: [
|
|
@@ -751,6 +1271,7 @@ var Editor = (props) => {
|
|
|
751
1271
|
className: "lib-xplat-editor__editable",
|
|
752
1272
|
onInput: handleInput,
|
|
753
1273
|
onKeyDown: handleKeyDown,
|
|
1274
|
+
onClick: handleEditorClick,
|
|
754
1275
|
onKeyUp: handleSelectionUpdate,
|
|
755
1276
|
onMouseUp: handleSelectionUpdate,
|
|
756
1277
|
onPaste: handlePaste,
|
|
@@ -792,14 +1313,27 @@ var TOOLBAR_LABEL = {
|
|
|
792
1313
|
underline: "U",
|
|
793
1314
|
strikethrough: "S",
|
|
794
1315
|
code: "<>",
|
|
795
|
-
heading: "
|
|
1316
|
+
"heading-1": "H1",
|
|
1317
|
+
"heading-2": "H2",
|
|
1318
|
+
"heading-3": "H3",
|
|
796
1319
|
list: "\u2022",
|
|
797
1320
|
"ordered-list": "1.",
|
|
798
1321
|
blockquote: "\u275D",
|
|
799
1322
|
"code-block": "[ ]",
|
|
800
1323
|
link: "\u{1F517}",
|
|
801
1324
|
image: "\u{1F5BC}",
|
|
802
|
-
divider: "\u2014"
|
|
1325
|
+
divider: "\u2014",
|
|
1326
|
+
checklist: "\u2611",
|
|
1327
|
+
"align-left": "\u21E4",
|
|
1328
|
+
"align-center": "\u2194",
|
|
1329
|
+
"align-right": "\u21E5",
|
|
1330
|
+
highlight: "\u{1F58D}",
|
|
1331
|
+
superscript: "x\xB2",
|
|
1332
|
+
subscript: "x\u2082",
|
|
1333
|
+
table: "\u25A6",
|
|
1334
|
+
"clear-format": "\u232B",
|
|
1335
|
+
undo: "\u21B6",
|
|
1336
|
+
redo: "\u21B7"
|
|
803
1337
|
};
|
|
804
1338
|
var TOOLBAR_TITLE = {
|
|
805
1339
|
bold: "\uAD75\uAC8C (\u2318B)",
|
|
@@ -807,36 +1341,74 @@ var TOOLBAR_TITLE = {
|
|
|
807
1341
|
underline: "\uBC11\uC904 (\u2318U)",
|
|
808
1342
|
strikethrough: "\uCDE8\uC18C\uC120",
|
|
809
1343
|
code: "\uC778\uB77C\uC778 \uCF54\uB4DC",
|
|
810
|
-
heading: "\uC81C\uBAA9",
|
|
1344
|
+
"heading-1": "\uC81C\uBAA9 1",
|
|
1345
|
+
"heading-2": "\uC81C\uBAA9 2",
|
|
1346
|
+
"heading-3": "\uC81C\uBAA9 3",
|
|
811
1347
|
list: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D",
|
|
812
1348
|
"ordered-list": "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D",
|
|
813
1349
|
blockquote: "\uC778\uC6A9",
|
|
814
1350
|
"code-block": "\uCF54\uB4DC \uBE14\uB85D",
|
|
815
1351
|
link: "\uB9C1\uD06C (\u2318K)",
|
|
816
1352
|
image: "\uC774\uBBF8\uC9C0",
|
|
817
|
-
divider: "\uAD6C\uBD84\uC120"
|
|
1353
|
+
divider: "\uAD6C\uBD84\uC120",
|
|
1354
|
+
checklist: "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8",
|
|
1355
|
+
"align-left": "\uC67C\uCABD \uC815\uB82C",
|
|
1356
|
+
"align-center": "\uAC00\uC6B4\uB370 \uC815\uB82C",
|
|
1357
|
+
"align-right": "\uC624\uB978\uCABD \uC815\uB82C",
|
|
1358
|
+
highlight: "\uD615\uAD11\uD39C",
|
|
1359
|
+
superscript: "\uC704 \uCCA8\uC790",
|
|
1360
|
+
subscript: "\uC544\uB798 \uCCA8\uC790",
|
|
1361
|
+
table: "\uD45C \uC0BD\uC785",
|
|
1362
|
+
"clear-format": "\uC11C\uC2DD \uC9C0\uC6B0\uAE30",
|
|
1363
|
+
undo: "\uB418\uB3CC\uB9AC\uAE30 (\u2318Z)",
|
|
1364
|
+
redo: "\uB2E4\uC2DC\uD558\uAE30 (\u2318\u21E7Z)"
|
|
818
1365
|
};
|
|
819
1366
|
var isActive = (item, active) => {
|
|
820
|
-
if (item === "heading") return active.has("block:h1")
|
|
1367
|
+
if (item === "heading-1") return active.has("block:h1");
|
|
1368
|
+
if (item === "heading-2") return active.has("block:h2");
|
|
1369
|
+
if (item === "heading-3") return active.has("block:h3");
|
|
821
1370
|
if (item === "blockquote") return active.has("block:blockquote");
|
|
822
1371
|
if (item === "code-block") return active.has("block:pre");
|
|
1372
|
+
if (item === "align-left") return active.has("align:left");
|
|
1373
|
+
if (item === "align-center") return active.has("align:center");
|
|
1374
|
+
if (item === "align-right") return active.has("align:right");
|
|
823
1375
|
return active.has(item);
|
|
824
1376
|
};
|
|
825
|
-
var EditorToolbar = ({
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1377
|
+
var EditorToolbar = ({
|
|
1378
|
+
items,
|
|
1379
|
+
active,
|
|
1380
|
+
onAction,
|
|
1381
|
+
codeLang,
|
|
1382
|
+
inCodeBlock,
|
|
1383
|
+
onCodeLangChange
|
|
1384
|
+
}) => {
|
|
1385
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "lib-xplat-editor__toolbar", children: [
|
|
1386
|
+
items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1387
|
+
"button",
|
|
1388
|
+
{
|
|
1389
|
+
type: "button",
|
|
1390
|
+
className: clsx_default("lib-xplat-editor__toolbar-btn", isActive(item, active) && "active"),
|
|
1391
|
+
title: TOOLBAR_TITLE[item],
|
|
1392
|
+
onMouseDown: (e) => {
|
|
1393
|
+
e.preventDefault();
|
|
1394
|
+
onAction(item);
|
|
1395
|
+
},
|
|
1396
|
+
children: TOOLBAR_LABEL[item]
|
|
835
1397
|
},
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1398
|
+
item
|
|
1399
|
+
)),
|
|
1400
|
+
inCodeBlock && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1401
|
+
"select",
|
|
1402
|
+
{
|
|
1403
|
+
className: "lib-xplat-editor__lang",
|
|
1404
|
+
value: codeLang,
|
|
1405
|
+
"aria-label": "\uCF54\uB4DC \uC5B8\uC5B4",
|
|
1406
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
1407
|
+
onChange: (e) => onCodeLangChange(e.target.value),
|
|
1408
|
+
children: CODE_LANGUAGES.map((l) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: l.value, children: l.label }, l.value))
|
|
1409
|
+
}
|
|
1410
|
+
)
|
|
1411
|
+
] });
|
|
840
1412
|
};
|
|
841
1413
|
var SlashMenu = ({ position, items, activeIndex, onSelect, onClose }) => {
|
|
842
1414
|
import_react.default.useEffect(() => {
|