@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
|
@@ -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,12 +213,51 @@ 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
|
};
|
|
252
|
+
var DROP_WITH_CONTENT = /* @__PURE__ */ new Set([
|
|
253
|
+
"script",
|
|
254
|
+
"style",
|
|
255
|
+
"noscript",
|
|
256
|
+
"iframe",
|
|
257
|
+
"object",
|
|
258
|
+
"embed",
|
|
259
|
+
"template"
|
|
260
|
+
]);
|
|
106
261
|
var sanitizeHtml = (input) => {
|
|
107
262
|
const doc = new DOMParser().parseFromString(`<div>${input}</div>`, "text/html");
|
|
108
263
|
const root = doc.body.firstElementChild;
|
|
@@ -111,6 +266,10 @@ var sanitizeHtml = (input) => {
|
|
|
111
266
|
const children = Array.from(node.children);
|
|
112
267
|
for (const child of children) {
|
|
113
268
|
const tag = child.tagName.toLowerCase();
|
|
269
|
+
if (DROP_WITH_CONTENT.has(tag)) {
|
|
270
|
+
node.removeChild(child);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
114
273
|
if (!ALLOWED_TAGS.has(tag)) {
|
|
115
274
|
while (child.firstChild) node.insertBefore(child.firstChild, child);
|
|
116
275
|
node.removeChild(child);
|
|
@@ -141,7 +300,9 @@ var escapeHtml = (text) => {
|
|
|
141
300
|
};
|
|
142
301
|
var SLASH_ITEMS = [
|
|
143
302
|
{ key: "paragraph", label: "\uB2E8\uB77D", hint: "\uAE30\uBCF8 \uD14D\uC2A4\uD2B8" },
|
|
144
|
-
{ 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: "" },
|
|
145
306
|
{ key: "list", label: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D", hint: "\u2022 \uD56D\uBAA9" },
|
|
146
307
|
{ key: "ordered-list", label: "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D", hint: "1. \uD56D\uBAA9" },
|
|
147
308
|
{ key: "blockquote", label: "\uC778\uC6A9", hint: "" },
|
|
@@ -175,12 +336,18 @@ var Editor = (props) => {
|
|
|
175
336
|
const [linkOpen, setLinkOpen] = import_react.default.useState(false);
|
|
176
337
|
const [linkValue, setLinkValue] = import_react.default.useState("");
|
|
177
338
|
const [linkPos, setLinkPos] = import_react.default.useState({ top: 0, left: 0 });
|
|
339
|
+
const lastEmittedRef = import_react.default.useRef(null);
|
|
178
340
|
import_react.default.useEffect(() => {
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
341
|
+
const el = editorRef.current;
|
|
342
|
+
if (!el) return;
|
|
343
|
+
if (value === lastEmittedRef.current) return;
|
|
344
|
+
if (el.innerHTML === value) return;
|
|
345
|
+
el.innerHTML = sanitizeHtml(value || "");
|
|
346
|
+
if (!el.firstChild) el.innerHTML = "<p><br></p>";
|
|
347
|
+
lastEmittedRef.current = null;
|
|
348
|
+
historyRef.current = [{ html: el.innerHTML, caret: 0 }];
|
|
349
|
+
historyIdxRef.current = 0;
|
|
350
|
+
updateEmpty();
|
|
184
351
|
}, [value]);
|
|
185
352
|
const updateEmpty = () => {
|
|
186
353
|
const el = editorRef.current;
|
|
@@ -196,10 +363,19 @@ var Editor = (props) => {
|
|
|
196
363
|
}
|
|
197
364
|
};
|
|
198
365
|
const restoreSelection = () => {
|
|
199
|
-
|
|
366
|
+
const range = lastRangeRef.current;
|
|
367
|
+
if (!range) return;
|
|
368
|
+
if (!editorRef.current?.contains(range.startContainer)) {
|
|
369
|
+
lastRangeRef.current = null;
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
200
372
|
const sel = window.getSelection();
|
|
201
|
-
|
|
202
|
-
|
|
373
|
+
try {
|
|
374
|
+
sel?.removeAllRanges();
|
|
375
|
+
sel?.addRange(range);
|
|
376
|
+
} catch {
|
|
377
|
+
lastRangeRef.current = null;
|
|
378
|
+
}
|
|
203
379
|
};
|
|
204
380
|
const updateActiveFormats = () => {
|
|
205
381
|
if (!editorRef.current) return;
|
|
@@ -215,6 +391,17 @@ var Editor = (props) => {
|
|
|
215
391
|
if (document.queryCommandState("insertOrderedList")) next.add("ordered-list");
|
|
216
392
|
const block = String(document.queryCommandValue("formatBlock") || "").toLowerCase();
|
|
217
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");
|
|
218
405
|
} catch {
|
|
219
406
|
}
|
|
220
407
|
setActiveFormats(next);
|
|
@@ -228,7 +415,374 @@ var Editor = (props) => {
|
|
|
228
415
|
const setBlock = (tag) => {
|
|
229
416
|
exec("formatBlock", tag);
|
|
230
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
|
+
};
|
|
534
|
+
const closestPre = () => {
|
|
535
|
+
const sel = window.getSelection();
|
|
536
|
+
const node = sel?.anchorNode;
|
|
537
|
+
if (!node || !editorRef.current?.contains(node)) return null;
|
|
538
|
+
const el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
539
|
+
return el?.closest("pre") ?? null;
|
|
540
|
+
};
|
|
541
|
+
const insertCodeBlock = () => {
|
|
542
|
+
pushHistory(true);
|
|
543
|
+
const el = editorRef.current;
|
|
544
|
+
const sel = window.getSelection();
|
|
545
|
+
if (!el || !sel || sel.rangeCount === 0) return;
|
|
546
|
+
const existing = closestPre();
|
|
547
|
+
if (existing) {
|
|
548
|
+
const p = document.createElement("p");
|
|
549
|
+
p.textContent = existing.textContent || "";
|
|
550
|
+
if (!p.textContent) p.innerHTML = "<br>";
|
|
551
|
+
existing.replaceWith(p);
|
|
552
|
+
placeCaretAtEnd(p);
|
|
553
|
+
handleInput();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const range = sel.getRangeAt(0);
|
|
557
|
+
const frag = range.cloneContents();
|
|
558
|
+
const holder = document.createElement("div");
|
|
559
|
+
holder.appendChild(frag);
|
|
560
|
+
const BLOCK = /^(p|div|h[1-6]|li|blockquote|pre|tr)$/i;
|
|
561
|
+
const linesOf = (node) => {
|
|
562
|
+
const out = [];
|
|
563
|
+
let cur = "";
|
|
564
|
+
for (const child of Array.from(node.childNodes)) {
|
|
565
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
566
|
+
cur += child.textContent || "";
|
|
567
|
+
} else if (child.nodeName === "BR") {
|
|
568
|
+
out.push(cur);
|
|
569
|
+
cur = "";
|
|
570
|
+
} else if (BLOCK.test(child.nodeName)) {
|
|
571
|
+
if (cur) {
|
|
572
|
+
out.push(cur);
|
|
573
|
+
cur = "";
|
|
574
|
+
}
|
|
575
|
+
const sub = linesOf(child);
|
|
576
|
+
out.push(...sub.length > 0 ? sub : [""]);
|
|
577
|
+
} else {
|
|
578
|
+
cur += linesOf(child).join("\n");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (cur) out.push(cur);
|
|
582
|
+
return out;
|
|
583
|
+
};
|
|
584
|
+
const text = linesOf(holder).join("\n").replace(/\n+$/, "");
|
|
585
|
+
const pre = document.createElement("pre");
|
|
586
|
+
const code = document.createElement("code");
|
|
587
|
+
code.textContent = text || "";
|
|
588
|
+
pre.appendChild(code);
|
|
589
|
+
range.deleteContents();
|
|
590
|
+
range.insertNode(pre);
|
|
591
|
+
ensureParagraphAfter(pre);
|
|
592
|
+
placeCaretAtEnd(code);
|
|
593
|
+
handleInput();
|
|
594
|
+
};
|
|
595
|
+
const insertTextAtCaret = (text) => {
|
|
596
|
+
const sel = window.getSelection();
|
|
597
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
598
|
+
const range = sel.getRangeAt(0);
|
|
599
|
+
range.deleteContents();
|
|
600
|
+
const node = document.createTextNode(text);
|
|
601
|
+
range.insertNode(node);
|
|
602
|
+
range.setStartAfter(node);
|
|
603
|
+
range.collapse(true);
|
|
604
|
+
sel.removeAllRanges();
|
|
605
|
+
sel.addRange(range);
|
|
606
|
+
handleInput();
|
|
607
|
+
};
|
|
608
|
+
const ensureParagraphAfter = (node) => {
|
|
609
|
+
const next = node.nextElementSibling;
|
|
610
|
+
if (next && next.tagName === "P") return;
|
|
611
|
+
const p = document.createElement("p");
|
|
612
|
+
p.innerHTML = "<br>";
|
|
613
|
+
node.after(p);
|
|
614
|
+
};
|
|
615
|
+
const placeCaretAtEnd = (node) => {
|
|
616
|
+
const sel = window.getSelection();
|
|
617
|
+
if (!sel) return;
|
|
618
|
+
const r2 = document.createRange();
|
|
619
|
+
r2.selectNodeContents(node);
|
|
620
|
+
r2.collapse(false);
|
|
621
|
+
sel.removeAllRanges();
|
|
622
|
+
sel.addRange(r2);
|
|
623
|
+
editorRef.current?.focus();
|
|
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
|
+
};
|
|
231
784
|
const insertDivider = () => {
|
|
785
|
+
pushHistory(true);
|
|
232
786
|
exec("insertHorizontalRule");
|
|
233
787
|
const el = editorRef.current;
|
|
234
788
|
if (el && el.lastElementChild?.tagName === "HR") {
|
|
@@ -274,7 +828,9 @@ var Editor = (props) => {
|
|
|
274
828
|
const range = sel.getRangeAt(0);
|
|
275
829
|
slashStartRangeRef.current = range.cloneRange();
|
|
276
830
|
const rect = range.getBoundingClientRect();
|
|
277
|
-
const
|
|
831
|
+
const editorEl = editorRef.current;
|
|
832
|
+
if (!editorEl) return;
|
|
833
|
+
const editorRect = editorEl.getBoundingClientRect();
|
|
278
834
|
setSlashPos({
|
|
279
835
|
top: rect.bottom - editorRect.top + 4,
|
|
280
836
|
left: rect.left - editorRect.left
|
|
@@ -319,8 +875,14 @@ var Editor = (props) => {
|
|
|
319
875
|
case "paragraph":
|
|
320
876
|
setBlock("p");
|
|
321
877
|
break;
|
|
322
|
-
case "heading":
|
|
323
|
-
|
|
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");
|
|
324
886
|
break;
|
|
325
887
|
case "list":
|
|
326
888
|
exec("insertUnorderedList");
|
|
@@ -332,7 +894,7 @@ var Editor = (props) => {
|
|
|
332
894
|
setBlock("blockquote");
|
|
333
895
|
break;
|
|
334
896
|
case "code-block":
|
|
335
|
-
|
|
897
|
+
insertCodeBlock();
|
|
336
898
|
break;
|
|
337
899
|
case "divider":
|
|
338
900
|
insertDivider();
|
|
@@ -350,7 +912,9 @@ var Editor = (props) => {
|
|
|
350
912
|
if (!sel || sel.rangeCount === 0) return;
|
|
351
913
|
const range = sel.getRangeAt(0);
|
|
352
914
|
const rect = range.getBoundingClientRect();
|
|
353
|
-
const
|
|
915
|
+
const editorEl2 = editorRef.current;
|
|
916
|
+
if (!editorEl2) return;
|
|
917
|
+
const editorRect = editorEl2.getBoundingClientRect();
|
|
354
918
|
setLinkPos({
|
|
355
919
|
top: rect.bottom - editorRect.top + 4,
|
|
356
920
|
left: rect.left - editorRect.left
|
|
@@ -383,13 +947,13 @@ var Editor = (props) => {
|
|
|
383
947
|
if (node.nodeType !== Node.TEXT_NODE) return false;
|
|
384
948
|
const text = (node.textContent || "").slice(0, range.startOffset);
|
|
385
949
|
const patterns = [
|
|
386
|
-
[
|
|
387
|
-
[
|
|
388
|
-
[
|
|
389
|
-
[
|
|
390
|
-
[
|
|
391
|
-
[/^1
|
|
392
|
-
[
|
|
950
|
+
[/^###$/, () => setBlock("h3")],
|
|
951
|
+
[/^##$/, () => setBlock("h2")],
|
|
952
|
+
[/^#$/, () => setBlock("h1")],
|
|
953
|
+
[/^-$/, () => exec("insertUnorderedList")],
|
|
954
|
+
[/^\*$/, () => exec("insertUnorderedList")],
|
|
955
|
+
[/^1\.$/, () => exec("insertOrderedList")],
|
|
956
|
+
[/^>$/, () => setBlock("blockquote")]
|
|
393
957
|
];
|
|
394
958
|
for (const [re, fn] of patterns) {
|
|
395
959
|
if (re.test(text)) {
|
|
@@ -409,7 +973,11 @@ var Editor = (props) => {
|
|
|
409
973
|
if (composingRef.current) return;
|
|
410
974
|
const el = editorRef.current;
|
|
411
975
|
if (!el) return;
|
|
412
|
-
|
|
976
|
+
const html = el.innerHTML;
|
|
977
|
+
lastEmittedRef.current = html;
|
|
978
|
+
pushHistory();
|
|
979
|
+
scheduleHighlight();
|
|
980
|
+
onChange?.(html);
|
|
413
981
|
updateEmpty();
|
|
414
982
|
updateActiveFormats();
|
|
415
983
|
if (slashOpen) {
|
|
@@ -437,6 +1005,40 @@ var Editor = (props) => {
|
|
|
437
1005
|
};
|
|
438
1006
|
const handleKeyDown = (e) => {
|
|
439
1007
|
if (readOnly) return;
|
|
1008
|
+
if (composingRef.current || e.nativeEvent.isComposing || e.keyCode === 229) return;
|
|
1009
|
+
const pre = closestPre();
|
|
1010
|
+
if (pre && !slashOpen) {
|
|
1011
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1012
|
+
const text = pre.textContent || "";
|
|
1013
|
+
if (text.endsWith("\n\n")) {
|
|
1014
|
+
e.preventDefault();
|
|
1015
|
+
const code = pre.querySelector("code") ?? pre;
|
|
1016
|
+
code.textContent = text.replace(/\n\n$/, "");
|
|
1017
|
+
ensureParagraphAfter(pre);
|
|
1018
|
+
const next = pre.nextElementSibling;
|
|
1019
|
+
if (next) placeCaretAtEnd(next);
|
|
1020
|
+
handleInput();
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
e.preventDefault();
|
|
1024
|
+
insertTextAtCaret("\n");
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
if (e.key === "Tab") {
|
|
1028
|
+
e.preventDefault();
|
|
1029
|
+
insertTextAtCaret(" ");
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
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
|
+
}
|
|
440
1042
|
if (slashOpen) {
|
|
441
1043
|
if (e.key === "ArrowDown") {
|
|
442
1044
|
e.preventDefault();
|
|
@@ -460,6 +1062,21 @@ var Editor = (props) => {
|
|
|
460
1062
|
return;
|
|
461
1063
|
}
|
|
462
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
|
+
}
|
|
463
1080
|
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
|
|
464
1081
|
const k = e.key.toLowerCase();
|
|
465
1082
|
if (k === "b") {
|
|
@@ -483,6 +1100,7 @@ var Editor = (props) => {
|
|
|
483
1100
|
return;
|
|
484
1101
|
}
|
|
485
1102
|
}
|
|
1103
|
+
if (pre) return;
|
|
486
1104
|
if (enableSlashCommand && e.key === "/") {
|
|
487
1105
|
setTimeout(openSlashMenu, 0);
|
|
488
1106
|
}
|
|
@@ -492,11 +1110,21 @@ var Editor = (props) => {
|
|
|
492
1110
|
}
|
|
493
1111
|
}
|
|
494
1112
|
};
|
|
1113
|
+
const plainPasteRef = import_react.default.useRef(false);
|
|
495
1114
|
const handlePaste = (e) => {
|
|
496
1115
|
if (readOnly) return;
|
|
497
1116
|
e.preventDefault();
|
|
498
|
-
const html = e.clipboardData.getData("text/html");
|
|
499
1117
|
const text = e.clipboardData.getData("text/plain");
|
|
1118
|
+
if (plainPasteRef.current) {
|
|
1119
|
+
plainPasteRef.current = false;
|
|
1120
|
+
insertTextAtCaret(text);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (closestPre()) {
|
|
1124
|
+
insertTextAtCaret(text);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const html = e.clipboardData.getData("text/html");
|
|
500
1128
|
const clean = html ? sanitizeHtml(html) : escapeHtml(text);
|
|
501
1129
|
document.execCommand("insertHTML", false, clean);
|
|
502
1130
|
handleInput();
|
|
@@ -518,8 +1146,11 @@ var Editor = (props) => {
|
|
|
518
1146
|
};
|
|
519
1147
|
const handleSelectionUpdate = () => {
|
|
520
1148
|
updateActiveFormats();
|
|
1149
|
+
const pre = closestPre();
|
|
1150
|
+
setCodeLang(pre ? langOf(pre) : "plain");
|
|
521
1151
|
};
|
|
522
1152
|
const onToolbarAction = (item) => {
|
|
1153
|
+
if (closestPre() && !["code-block", "undo", "redo"].includes(item)) return;
|
|
523
1154
|
switch (item) {
|
|
524
1155
|
case "bold":
|
|
525
1156
|
exec("bold");
|
|
@@ -548,8 +1179,14 @@ var Editor = (props) => {
|
|
|
548
1179
|
handleInput();
|
|
549
1180
|
break;
|
|
550
1181
|
}
|
|
551
|
-
case "heading":
|
|
552
|
-
|
|
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");
|
|
553
1190
|
break;
|
|
554
1191
|
case "list":
|
|
555
1192
|
exec("insertUnorderedList");
|
|
@@ -561,7 +1198,7 @@ var Editor = (props) => {
|
|
|
561
1198
|
setBlock("blockquote");
|
|
562
1199
|
break;
|
|
563
1200
|
case "code-block":
|
|
564
|
-
|
|
1201
|
+
insertCodeBlock();
|
|
565
1202
|
break;
|
|
566
1203
|
case "link":
|
|
567
1204
|
openLinkEditor();
|
|
@@ -572,6 +1209,39 @@ var Editor = (props) => {
|
|
|
572
1209
|
case "divider":
|
|
573
1210
|
insertDivider();
|
|
574
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;
|
|
575
1245
|
}
|
|
576
1246
|
};
|
|
577
1247
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: clsx_default("lib-xplat-editor", isFocused && "focused", readOnly && "readonly"), children: [
|
|
@@ -580,7 +1250,10 @@ var Editor = (props) => {
|
|
|
580
1250
|
{
|
|
581
1251
|
items: toolbar,
|
|
582
1252
|
active: activeFormats,
|
|
583
|
-
onAction: onToolbarAction
|
|
1253
|
+
onAction: onToolbarAction,
|
|
1254
|
+
codeLang,
|
|
1255
|
+
inCodeBlock: activeFormats.has("block:pre"),
|
|
1256
|
+
onCodeLangChange: changeCodeLang
|
|
584
1257
|
}
|
|
585
1258
|
),
|
|
586
1259
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "lib-xplat-editor__content", style: { minHeight }, children: [
|
|
@@ -591,9 +1264,14 @@ var Editor = (props) => {
|
|
|
591
1264
|
ref: editorRef,
|
|
592
1265
|
contentEditable: !readOnly,
|
|
593
1266
|
suppressContentEditableWarning: true,
|
|
1267
|
+
role: "textbox",
|
|
1268
|
+
"aria-multiline": "true",
|
|
1269
|
+
"aria-readonly": readOnly || void 0,
|
|
1270
|
+
"aria-label": placeholder,
|
|
594
1271
|
className: "lib-xplat-editor__editable",
|
|
595
1272
|
onInput: handleInput,
|
|
596
1273
|
onKeyDown: handleKeyDown,
|
|
1274
|
+
onClick: handleEditorClick,
|
|
597
1275
|
onKeyUp: handleSelectionUpdate,
|
|
598
1276
|
onMouseUp: handleSelectionUpdate,
|
|
599
1277
|
onPaste: handlePaste,
|
|
@@ -635,14 +1313,27 @@ var TOOLBAR_LABEL = {
|
|
|
635
1313
|
underline: "U",
|
|
636
1314
|
strikethrough: "S",
|
|
637
1315
|
code: "<>",
|
|
638
|
-
heading: "
|
|
1316
|
+
"heading-1": "H1",
|
|
1317
|
+
"heading-2": "H2",
|
|
1318
|
+
"heading-3": "H3",
|
|
639
1319
|
list: "\u2022",
|
|
640
1320
|
"ordered-list": "1.",
|
|
641
1321
|
blockquote: "\u275D",
|
|
642
1322
|
"code-block": "[ ]",
|
|
643
1323
|
link: "\u{1F517}",
|
|
644
1324
|
image: "\u{1F5BC}",
|
|
645
|
-
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"
|
|
646
1337
|
};
|
|
647
1338
|
var TOOLBAR_TITLE = {
|
|
648
1339
|
bold: "\uAD75\uAC8C (\u2318B)",
|
|
@@ -650,36 +1341,74 @@ var TOOLBAR_TITLE = {
|
|
|
650
1341
|
underline: "\uBC11\uC904 (\u2318U)",
|
|
651
1342
|
strikethrough: "\uCDE8\uC18C\uC120",
|
|
652
1343
|
code: "\uC778\uB77C\uC778 \uCF54\uB4DC",
|
|
653
|
-
heading: "\uC81C\uBAA9",
|
|
1344
|
+
"heading-1": "\uC81C\uBAA9 1",
|
|
1345
|
+
"heading-2": "\uC81C\uBAA9 2",
|
|
1346
|
+
"heading-3": "\uC81C\uBAA9 3",
|
|
654
1347
|
list: "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D",
|
|
655
1348
|
"ordered-list": "\uBC88\uD638 \uB9E4\uAE30\uAE30 \uBAA9\uB85D",
|
|
656
1349
|
blockquote: "\uC778\uC6A9",
|
|
657
1350
|
"code-block": "\uCF54\uB4DC \uBE14\uB85D",
|
|
658
1351
|
link: "\uB9C1\uD06C (\u2318K)",
|
|
659
1352
|
image: "\uC774\uBBF8\uC9C0",
|
|
660
|
-
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)"
|
|
661
1365
|
};
|
|
662
1366
|
var isActive = (item, active) => {
|
|
663
|
-
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");
|
|
664
1370
|
if (item === "blockquote") return active.has("block:blockquote");
|
|
665
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");
|
|
666
1375
|
return active.has(item);
|
|
667
1376
|
};
|
|
668
|
-
var EditorToolbar = ({
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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]
|
|
678
1397
|
},
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
+
] });
|
|
683
1412
|
};
|
|
684
1413
|
var SlashMenu = ({ position, items, activeIndex, onSelect, onClose }) => {
|
|
685
1414
|
import_react.default.useEffect(() => {
|