@comitor/ui 3.5.0 → 3.6.1
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/README.md +4 -2
- package/dist/data-grid/data-grid-cell-variants.js +6 -1
- package/dist/data-grid/data-grid-cell-variants.js.map +1 -1
- package/dist/data-grid/types.d.ts +10 -0
- package/dist/data-grid/use-data-grid.d.ts +2 -2
- package/dist/data-grid/use-data-grid.js +30 -13
- package/dist/data-grid/use-data-grid.js.map +1 -1
- package/dist/editor/bubble-toolbar.d.ts +20 -1
- package/dist/editor/bubble-toolbar.js +12 -172
- package/dist/editor/bubble-toolbar.js.map +1 -1
- package/dist/editor/editor-core.d.ts +28 -0
- package/dist/editor/editor-core.js +354 -0
- package/dist/editor/editor-core.js.map +1 -0
- package/dist/editor/editor-types.d.ts +3 -0
- package/dist/editor/editor-types.js +1 -0
- package/dist/editor/editor-types.js.map +1 -0
- package/dist/editor/fixed-toolbar.d.ts +19 -0
- package/dist/editor/fixed-toolbar.js +106 -0
- package/dist/editor/fixed-toolbar.js.map +1 -0
- package/dist/editor/image-upload.d.ts +3 -57
- package/dist/editor/image-upload.js +3 -2
- package/dist/editor/image-upload.js.map +1 -1
- package/dist/editor/index.d.ts +3 -2
- package/dist/editor/index.js +4 -4
- package/dist/editor/index.js.map +1 -1
- package/dist/editor/markdown-editor-classes.js +6 -4
- package/dist/editor/markdown-editor-classes.js.map +1 -1
- package/dist/editor/markdown-editor-constants.d.ts +8 -2
- package/dist/editor/markdown-editor-constants.js.map +1 -1
- package/dist/editor/markdown-editor.d.ts +14 -91
- package/dist/editor/markdown-editor.js +23 -315
- package/dist/editor/markdown-editor.js.map +1 -1
- package/dist/editor/markdown-extensions.d.ts +40 -15
- package/dist/editor/markdown-extensions.js +143 -24
- package/dist/editor/markdown-extensions.js.map +1 -1
- package/dist/editor/rich-text-editor.d.ts +48 -0
- package/dist/editor/rich-text-editor.js +33 -0
- package/dist/editor/rich-text-editor.js.map +1 -0
- package/dist/editor/slash-commands.d.ts +4 -1
- package/dist/editor/slash-commands.js +7 -2
- package/dist/editor/slash-commands.js.map +1 -1
- package/dist/editor/toolbar-actions.d.ts +44 -0
- package/dist/editor/toolbar-actions.js +173 -0
- package/dist/editor/toolbar-actions.js.map +1 -0
- package/dist/editor/toolbar-button.d.ts +15 -0
- package/dist/editor/toolbar-button.js +47 -0
- package/dist/editor/toolbar-button.js.map +1 -0
- package/dist/editor-types-BMST7-VR.d.ts +171 -0
- package/dist/i18n/namespaces/markdown-editor.js +6 -0
- package/dist/i18n/namespaces/markdown-editor.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,321 +1,29 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
createImageUploadExtension,
|
|
16
|
-
insertImageFiles
|
|
17
|
-
} from "./image-upload.js";
|
|
18
|
-
import { MARKDOWN_EDITOR_CONTENT_CLASS } from "./markdown-editor-classes.js";
|
|
19
|
-
import {
|
|
20
|
-
createEditorKeymap,
|
|
21
|
-
createMarkdownSchemaExtensions
|
|
22
|
-
} from "./markdown-extensions.js";
|
|
23
|
-
import { trimMarkdown } from "./markdown-syntax.js";
|
|
24
|
-
import {
|
|
25
|
-
createSlashCommandExtension,
|
|
26
|
-
filterSlashCommands,
|
|
27
|
-
SLASH_PLUGIN_KEY
|
|
28
|
-
} from "./slash-commands.js";
|
|
29
|
-
import { SuggestionMenu } from "./suggestion-menu.js";
|
|
30
|
-
import { SuggestionStore } from "./suggestion-store.js";
|
|
31
|
-
const MENTION_PLUGIN_KEY = new PluginKey("comitorMentionMenu");
|
|
32
|
-
const MENTION_LIST_LIMIT = 50;
|
|
33
|
-
function filterMentionItems(items, query) {
|
|
34
|
-
const term = query.trim();
|
|
35
|
-
const matches = term ? items.filter(
|
|
36
|
-
(item) => matchesSearch(item.label, term) || // Đi QUA từ đầu của tên: "@nguyenvana" vẫn tới "Nguyễn Văn An" — cùng luật `Mention`.
|
|
37
|
-
matchesSearch(item.label.replace(/\s+/g, ""), term.replace(/\s+/g, "")) || (item.description ? matchesSearch(item.description, term) : false)
|
|
38
|
-
) : items;
|
|
39
|
-
return matches.slice(0, MENTION_LIST_LIMIT);
|
|
40
|
-
}
|
|
41
|
-
function readMarkdown(editor) {
|
|
42
|
-
return trimMarkdown(editor.getMarkdown());
|
|
43
|
-
}
|
|
44
|
-
const toBoolean = (value) => value === true || value === "true";
|
|
45
|
-
function MarkdownEditor({
|
|
46
|
-
value,
|
|
47
|
-
defaultValue,
|
|
48
|
-
onChange,
|
|
49
|
-
onSubmit,
|
|
50
|
-
onFocus,
|
|
51
|
-
onBlur,
|
|
52
|
-
placeholder,
|
|
53
|
-
editable = true,
|
|
54
|
-
autoFocus = false,
|
|
55
|
-
variant = "outline",
|
|
56
|
-
mentions,
|
|
57
|
-
onImageUpload,
|
|
58
|
-
onImageUploadError,
|
|
59
|
-
resolveImageSrc,
|
|
60
|
-
extensions: extraExtensions,
|
|
61
|
-
labels: labelsProp,
|
|
62
|
-
name,
|
|
63
|
-
id,
|
|
64
|
-
"aria-label": ariaLabel,
|
|
65
|
-
"aria-labelledby": ariaLabelledBy,
|
|
66
|
-
"aria-describedby": ariaDescribedBy,
|
|
67
|
-
"aria-invalid": ariaInvalid,
|
|
68
|
-
"aria-required": ariaRequired,
|
|
69
|
-
className,
|
|
70
|
-
contentClassName,
|
|
71
|
-
ref
|
|
72
|
-
}) {
|
|
73
|
-
const labels = useComitorLabels("markdownEditor", labelsProp);
|
|
74
|
-
const runtime = useAsRef({
|
|
75
|
-
placeholder: placeholder ?? labels.placeholder,
|
|
76
|
-
taskCheckbox: labels.taskCheckbox,
|
|
77
|
-
resolveImageSrc,
|
|
78
|
-
resolveMentionLabel: mentions?.resolveLabel
|
|
79
|
-
});
|
|
80
|
-
const latest = useAsRef({ onChange, onFocus, onBlur, mentions, onImageUpload, labels, name });
|
|
81
|
-
const imageHandlers = useAsRef({
|
|
82
|
-
upload: onImageUpload,
|
|
83
|
-
onError: (error, file) => {
|
|
84
|
-
if (onImageUploadError) onImageUploadError(error, file);
|
|
85
|
-
else toast(labels.imageUploadFailed, { variant: "destructive", description: file.name || void 0 });
|
|
86
|
-
},
|
|
87
|
-
resolveSrc: resolveImageSrc,
|
|
88
|
-
uploadingLabel: labels.imageUploading
|
|
89
|
-
});
|
|
90
|
-
const [linkRequest, setLinkRequest] = React.useState(0);
|
|
91
|
-
const keymapHandlers = useAsRef({
|
|
92
|
-
onLinkShortcut: () => {
|
|
93
|
-
if (!editable) return false;
|
|
94
|
-
setLinkRequest((count) => count + 1);
|
|
95
|
-
return true;
|
|
96
|
-
},
|
|
97
|
-
onSubmitShortcut: () => {
|
|
98
|
-
if (!onSubmit) return false;
|
|
99
|
-
onSubmit();
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
const initialMarkdown = value ?? defaultValue ?? "";
|
|
104
|
-
const lastMarkdown = React.useRef(initialMarkdown);
|
|
105
|
-
const [hiddenValue, setHiddenValue] = React.useState(initialMarkdown);
|
|
106
|
-
const pendingUploads = useLazyRef(() => /* @__PURE__ */ new Set()).current;
|
|
107
|
-
const fileInputRef = React.useRef(null);
|
|
108
|
-
const slashStore = useLazyRef(
|
|
109
|
-
() => new SuggestionStore(SLASH_PLUGIN_KEY, (item) => item)
|
|
110
|
-
).current;
|
|
111
|
-
const mentionStore = useLazyRef(
|
|
112
|
-
() => new SuggestionStore(MENTION_PLUGIN_KEY, (item) => ({
|
|
113
|
-
id: item.id,
|
|
114
|
-
label: item.label
|
|
115
|
-
}))
|
|
116
|
-
).current;
|
|
117
|
-
const [extensions] = React.useState(() => [
|
|
118
|
-
...createMarkdownSchemaExtensions({
|
|
119
|
-
runtime,
|
|
120
|
-
mentionSuggestion: {
|
|
121
|
-
pluginKey: MENTION_PLUGIN_KEY,
|
|
122
|
-
char: "@",
|
|
123
|
-
// Tên người Việt có dấu cách. Khi chữ gõ có dấu cách mà không còn ai khớp, menu tự thoát
|
|
124
|
-
// (`exitWhenEmpty`) — người dùng đang viết câu, không còn tìm người nữa.
|
|
125
|
-
allowSpaces: true,
|
|
126
|
-
debounce: typeof mentions?.items === "function" ? 150 : 0,
|
|
127
|
-
items: async ({ query, signal }) => {
|
|
128
|
-
const source = latest.current.mentions?.items;
|
|
129
|
-
if (!source) return [];
|
|
130
|
-
if (typeof source === "function") return [...await source(query, { signal })];
|
|
131
|
-
return filterMentionItems(source, query);
|
|
132
|
-
},
|
|
133
|
-
allow: ({ editor: current }) => Boolean(latest.current.mentions) && current.isEditable && !current.isActive("codeBlock") && !current.isActive("code"),
|
|
134
|
-
render: mentionStore.render
|
|
135
|
-
}
|
|
136
|
-
}),
|
|
137
|
-
createSlashCommandExtension({
|
|
138
|
-
items: ({ editor: current, query }) => filterSlashCommands(current, query, latest.current.labels, {
|
|
139
|
-
pickImage: latest.current.onImageUpload ? () => fileInputRef.current?.click() : void 0
|
|
140
|
-
}),
|
|
141
|
-
render: slashStore.render
|
|
142
|
-
}),
|
|
143
|
-
createImageUploadExtension(imageHandlers, pendingUploads),
|
|
144
|
-
createEditorKeymap(keymapHandlers),
|
|
145
|
-
...extraExtensions ?? []
|
|
146
|
-
]);
|
|
147
|
-
const [labelElementId, setLabelElementId] = React.useState(null);
|
|
148
|
-
const editorProps = React.useMemo(() => {
|
|
149
|
-
const attributes = {
|
|
150
|
-
class: cn(
|
|
151
|
-
MARKDOWN_EDITOR_CONTENT_CLASS,
|
|
152
|
-
variant === "outline" ? "min-h-16 px-3 py-2" : "min-h-6",
|
|
153
|
-
contentClassName
|
|
154
|
-
)
|
|
155
|
-
};
|
|
156
|
-
if (id) attributes.id = id;
|
|
157
|
-
if (!editable) attributes.role = "none";
|
|
158
|
-
if (editable) {
|
|
159
|
-
attributes.role = "textbox";
|
|
160
|
-
attributes["aria-multiline"] = "true";
|
|
161
|
-
const labelledBy = ariaLabelledBy ?? labelElementId;
|
|
162
|
-
if (ariaLabel || !labelledBy) attributes["aria-label"] = ariaLabel ?? labels.editorLabel;
|
|
163
|
-
if (labelledBy) attributes["aria-labelledby"] = labelledBy;
|
|
164
|
-
if (toBoolean(ariaRequired)) attributes["aria-required"] = "true";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { EditorCore } from "./editor-core.js";
|
|
4
|
+
function createMarkdownHandle(core) {
|
|
5
|
+
return {
|
|
6
|
+
focus: core.focus,
|
|
7
|
+
getMarkdown: core.getValue,
|
|
8
|
+
setMarkdown: core.setValue,
|
|
9
|
+
clear: core.clear,
|
|
10
|
+
isEmpty: core.isEmpty,
|
|
11
|
+
isUploading: core.isUploading,
|
|
12
|
+
get editor() {
|
|
13
|
+
return core.editor;
|
|
165
14
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
contentClassName,
|
|
172
|
-
id,
|
|
173
|
-
editable,
|
|
174
|
-
ariaLabel,
|
|
175
|
-
ariaLabelledBy,
|
|
176
|
-
labelElementId,
|
|
177
|
-
ariaDescribedBy,
|
|
178
|
-
ariaInvalid,
|
|
179
|
-
ariaRequired,
|
|
180
|
-
labels.editorLabel
|
|
181
|
-
]);
|
|
182
|
-
const editor = useEditor({
|
|
183
|
-
extensions,
|
|
184
|
-
content: initialMarkdown,
|
|
185
|
-
contentType: "markdown",
|
|
186
|
-
editable,
|
|
187
|
-
autofocus: autoFocus ? "end" : false,
|
|
188
|
-
// Next App Router render ở máy chủ: dựng trình soạn ở client sau lần hydrate đầu, không thì HTML
|
|
189
|
-
// hai phía lệch nhau. Khung rỗng giữ chỗ trong khoảng đó — xem phần render.
|
|
190
|
-
immediatelyRender: false,
|
|
191
|
-
editorProps,
|
|
192
|
-
onUpdate: ({ editor: current }) => {
|
|
193
|
-
const markdown = readMarkdown(current);
|
|
194
|
-
lastMarkdown.current = markdown;
|
|
195
|
-
latest.current.onChange?.(markdown);
|
|
196
|
-
if (latest.current.name) setHiddenValue(markdown);
|
|
197
|
-
},
|
|
198
|
-
onFocus: () => latest.current.onFocus?.(),
|
|
199
|
-
onBlur: () => latest.current.onBlur?.()
|
|
200
|
-
});
|
|
201
|
-
React.useEffect(() => {
|
|
202
|
-
if (!editor || value === void 0 || value === lastMarkdown.current) return;
|
|
203
|
-
editor.commands.setContent(value, { contentType: "markdown", emitUpdate: false });
|
|
204
|
-
lastMarkdown.current = value;
|
|
205
|
-
setHiddenValue(value);
|
|
206
|
-
}, [editor, value]);
|
|
207
|
-
React.useEffect(() => {
|
|
208
|
-
if (editor && editor.isEditable !== editable) editor.setEditable(editable, false);
|
|
209
|
-
}, [editor, editable]);
|
|
210
|
-
React.useEffect(() => {
|
|
211
|
-
if (!editor || !id || !editable || ariaLabel || ariaLabelledBy) return;
|
|
212
|
-
const label = document.querySelector(`label[for="${CSS.escape(id)}"]`);
|
|
213
|
-
if (!label) return;
|
|
214
|
-
if (!label.id) label.id = `${id}-label`;
|
|
215
|
-
setLabelElementId(label.id);
|
|
216
|
-
const focusEditor = (event) => {
|
|
217
|
-
event.preventDefault();
|
|
218
|
-
editor.commands.focus();
|
|
219
|
-
};
|
|
220
|
-
label.addEventListener("click", focusEditor);
|
|
221
|
-
return () => {
|
|
222
|
-
label.removeEventListener("click", focusEditor);
|
|
223
|
-
setLabelElementId(null);
|
|
224
|
-
};
|
|
225
|
-
}, [editor, id, editable, ariaLabel, ariaLabelledBy]);
|
|
226
|
-
const editorRef = useAsRef(editor);
|
|
227
|
-
React.useImperativeHandle(
|
|
228
|
-
ref,
|
|
229
|
-
() => ({
|
|
230
|
-
focus: (position = "end") => {
|
|
231
|
-
editorRef.current?.commands.focus(position);
|
|
232
|
-
},
|
|
233
|
-
getMarkdown: () => editorRef.current ? readMarkdown(editorRef.current) : lastMarkdown.current,
|
|
234
|
-
setMarkdown: (markdown) => {
|
|
235
|
-
editorRef.current?.commands.setContent(markdown, { contentType: "markdown", emitUpdate: true });
|
|
236
|
-
},
|
|
237
|
-
clear: () => {
|
|
238
|
-
editorRef.current?.commands.clearContent(true);
|
|
239
|
-
},
|
|
240
|
-
isEmpty: () => editorRef.current ? editorRef.current.isEmpty : lastMarkdown.current.trim() === "",
|
|
241
|
-
isUploading: () => pendingUploads.size > 0,
|
|
242
|
-
get editor() {
|
|
243
|
-
return editorRef.current;
|
|
244
|
-
}
|
|
245
|
-
}),
|
|
246
|
-
[editorRef, pendingUploads]
|
|
247
|
-
);
|
|
248
|
-
return /* @__PURE__ */ jsxs(
|
|
249
|
-
"div",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function MarkdownEditor({ toolbar = "bubble", ref, ...props }) {
|
|
18
|
+
return /* @__PURE__ */ jsx(
|
|
19
|
+
EditorCore,
|
|
250
20
|
{
|
|
251
|
-
|
|
252
|
-
"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// focus `--primary-ink` + vòng `ring/50`; lỗi đọc từ `aria-invalid` của CHÍNH vùng soạn.
|
|
258
|
-
"rounded-md border border-input bg-transparent shadow-xs transition-colors duration-150 ease-out focus-within:border-primary-ink focus-within:ring-2 focus-within:ring-ring/50 has-[[aria-invalid=true]]:border-destructive-ink dark:bg-input-fill",
|
|
259
|
-
className
|
|
260
|
-
),
|
|
261
|
-
children: [
|
|
262
|
-
editor ? /* @__PURE__ */ jsx(EditorContent, { editor }) : (
|
|
263
|
-
// Giữ chỗ tới khi trình soạn dựng xong ở client (`immediatelyRender: false`) — cùng chiều cao tối thiểu.
|
|
264
|
-
/* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: variant === "outline" ? "min-h-16 px-3 py-2" : "min-h-6" })
|
|
265
|
-
),
|
|
266
|
-
editor && editable && /* @__PURE__ */ jsx(BubbleToolbar, { editor, labels, linkRequest }),
|
|
267
|
-
editor && /* @__PURE__ */ jsx(
|
|
268
|
-
SuggestionMenu,
|
|
269
|
-
{
|
|
270
|
-
editor,
|
|
271
|
-
store: slashStore,
|
|
272
|
-
label: labels.slashMenu,
|
|
273
|
-
emptyLabel: labels.slashEmpty,
|
|
274
|
-
getKey: (item) => item.id,
|
|
275
|
-
renderItem: (item) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
276
|
-
/* @__PURE__ */ jsx(item.icon, { "aria-hidden": "true" }),
|
|
277
|
-
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
|
|
278
|
-
item.hint && // Cú pháp gõ tắt — mờ, chữ đều, như phím tắt trong `CommandShortcut`.
|
|
279
|
-
/* @__PURE__ */ jsx("span", { className: "ml-auto font-mono text-xs text-muted-foreground", children: item.hint })
|
|
280
|
-
] })
|
|
281
|
-
}
|
|
282
|
-
),
|
|
283
|
-
editor && mentions && /* @__PURE__ */ jsx(
|
|
284
|
-
SuggestionMenu,
|
|
285
|
-
{
|
|
286
|
-
editor,
|
|
287
|
-
store: mentionStore,
|
|
288
|
-
label: labels.mentionMenu,
|
|
289
|
-
emptyLabel: labels.mentionEmpty,
|
|
290
|
-
loadingLabel: labels.mentionLoading,
|
|
291
|
-
getKey: (item) => item.id,
|
|
292
|
-
exitWhenEmpty: (query) => /\s/.test(query),
|
|
293
|
-
renderItem: (item) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
294
|
-
/* @__PURE__ */ jsx(LetterAvatar, { name: item.label, src: item.avatarUrl, size: "xs", "aria-hidden": "true" }),
|
|
295
|
-
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
|
|
296
|
-
item.description && /* @__PURE__ */ jsx("span", { className: "max-w-[45%] truncate text-xs text-muted-foreground", children: item.description })
|
|
297
|
-
] })
|
|
298
|
-
}
|
|
299
|
-
),
|
|
300
|
-
onImageUpload && /* @__PURE__ */ jsx(
|
|
301
|
-
"input",
|
|
302
|
-
{
|
|
303
|
-
ref: fileInputRef,
|
|
304
|
-
type: "file",
|
|
305
|
-
accept: "image/*",
|
|
306
|
-
multiple: true,
|
|
307
|
-
hidden: true,
|
|
308
|
-
tabIndex: -1,
|
|
309
|
-
onChange: (event) => {
|
|
310
|
-
const files = Array.from(event.target.files ?? []).filter((file) => file.type.startsWith("image/"));
|
|
311
|
-
event.target.value = "";
|
|
312
|
-
if (editor && files.length > 0)
|
|
313
|
-
insertImageFiles(editor, files, void 0, imageHandlers.current, pendingUploads);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
),
|
|
317
|
-
name && /* @__PURE__ */ jsx("input", { type: "hidden", name, value: hiddenValue })
|
|
318
|
-
]
|
|
21
|
+
...props,
|
|
22
|
+
format: "markdown",
|
|
23
|
+
toolbar,
|
|
24
|
+
slot: "markdown-editor",
|
|
25
|
+
handleRef: ref,
|
|
26
|
+
createHandle: createMarkdownHandle
|
|
319
27
|
}
|
|
320
28
|
);
|
|
321
29
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/editor/markdown-editor.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * MarkdownEditor — trình soạn văn bản giàu định dạng kiểu Linear, LƯU bằng markdown.\n *\n * Soạn trực tiếp trên bản đã định dạng (không có khung \"Xem trước\" tách đôi): gõ `## ` là thành\n * tiêu đề, `- ` là danh sách, `[] ` là danh sách việc, ```` ``` ```` là khối mã; `/` mở menu khối,\n * `@` mở danh sách người, bôi chọn chữ là hiện thanh định dạng. Giá trị vào/ra là CHUỖI markdown\n * (GFM) — thứ module lưu ở cột mô tả và thứ công cụ MCP đọc/ghi.\n *\n * Dựng trên Tiptap 3 (ProseMirror — cùng nền với trình soạn của Linear). Lý do chọn, các phương án\n * đã loại, và những chỗ đã vá `@tiptap/markdown`: docs/editor.md.\n *\n * ⚠ Mọi gói `@tiptap/*` là peer TUỲ CHỌN của entry `@comitor/ui/editor` và phải CÙNG MỘT phiên bản\n * (chúng ghim nhau bằng số chính xác). Lệnh cài ở `src/editor/index.ts`.\n */\n\nimport type { AnyExtension, Editor } from \"@tiptap/core\";\nimport { PluginKey } from \"@tiptap/pm/state\";\nimport { EditorContent, useEditor } from \"@tiptap/react\";\nimport * as React from \"react\";\nimport { LetterAvatar } from \"../components/composite/letter-avatar.js\";\nimport { toast } from \"../components/composite/toast.js\";\nimport { useComitorLabels } from \"../i18n/locale-provider.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { matchesSearch } from \"../lib/text-search.js\";\nimport { useAsRef } from \"../lib/use-as-ref.js\";\nimport { useLazyRef } from \"../lib/use-lazy-ref.js\";\nimport { BubbleToolbar } from \"./bubble-toolbar.js\";\nimport {\n createImageUploadExtension,\n type ImageUploadHandlers,\n insertImageFiles,\n type MarkdownEditorImageUploadResult,\n type PendingUploads\n} from \"./image-upload.js\";\nimport { MARKDOWN_EDITOR_CONTENT_CLASS } from \"./markdown-editor-classes.js\";\nimport type { MarkdownEditorLabels } from \"./markdown-editor-constants.js\";\nimport {\n createEditorKeymap,\n createMarkdownSchemaExtensions,\n type EditorKeymapHandlers,\n type MarkdownEditorRuntime\n} from \"./markdown-extensions.js\";\nimport { trimMarkdown } from \"./markdown-syntax.js\";\nimport {\n createSlashCommandExtension,\n filterSlashCommands,\n SLASH_PLUGIN_KEY,\n type SlashMenuItem\n} from \"./slash-commands.js\";\nimport { SuggestionMenu } from \"./suggestion-menu.js\";\nimport { SuggestionStore } from \"./suggestion-store.js\";\n\n/** Một người trong danh sách `@`. */\nexport interface MarkdownEditorMentionItem {\n /** Định danh LƯU vào markdown (`[@Tên](mention:<id>)`) — với module Comitor là id người dùng của Account */\n id: string;\n /** Tên hiển thị lúc chọn — cũng là ảnh chụp tên ghi kèm trong markdown */\n label: string;\n /** Dòng phụ mờ bên phải (email, chức danh) — cũng được dùng để lọc */\n description?: string;\n avatarUrl?: string | null;\n}\n\n/**\n * Nguồn danh sách `@`:\n * · MẢNG — gói tự lọc theo tên/dòng phụ, không dấu (cùng luật `Combobox`), tối đa 50 người;\n * · HÀM — app tự tìm (gọi API danh bạ…). `signal` bị huỷ khi người dùng gõ tiếp; gói chờ 150ms\n * sau phím cuối mới gọi. (Kiểu nguồn xét LÚC MOUNT — độ trễ là tuỳ chọn dựng của plugin.)\n */\nexport type MarkdownEditorMentionSource =\n | readonly MarkdownEditorMentionItem[]\n | ((\n query: string,\n options: { signal: AbortSignal }\n ) => readonly MarkdownEditorMentionItem[] | Promise<readonly MarkdownEditorMentionItem[]>);\n\nexport interface MarkdownEditorMentions {\n items: MarkdownEditorMentionSource;\n /**\n * Tên SỐNG theo id, dùng lúc VẼ. Markdown giữ tên chụp lúc nhắc; người đó đổi tên ở Account thì\n * hàm này cho tên mới mà không phải sửa nội dung. `undefined` → rơi về tên chụp.\n */\n resolveLabel?: (id: string) => string | undefined;\n}\n\n/** Điều khiển trình soạn từ ngoài — lấy qua prop `ref`. */\nexport interface MarkdownEditorHandle {\n focus: (position?: \"start\" | \"end\") => void;\n /** Markdown hiện tại — cùng chuỗi mà `onChange` nhận. */\n getMarkdown: () => string;\n /** Thay toàn bộ nội dung. Có gọi `onChange`. */\n setMarkdown: (markdown: string) => void;\n /** Xoá trắng — vd sau khi gửi bình luận. Có gọi `onChange(\"\")`. */\n clear: () => void;\n isEmpty: () => boolean;\n /**\n * Còn ảnh đang tải lên không. Ảnh chưa tải xong KHÔNG có trong markdown — nút Gửi nên chờ (hoặc\n * khoá) khi hàm này trả `true`, nếu không bình luận đi mà thiếu ảnh.\n */\n isUploading: () => boolean;\n /** Bản Tiptap bên dưới — lối thoát cho nhu cầu gói chưa phủ. `null` trước lần render đầu ở client. */\n readonly editor: Editor | null;\n}\n\nexport type MarkdownEditorVariant = \"outline\" | \"ghost\";\n\nexport interface MarkdownEditorProps {\n /** Markdown — điều khiển từ ngoài. Đổi giá trị KHÁC thứ trình soạn vừa phát là nạp lại nội dung. */\n value?: string;\n /** Markdown ban đầu khi KHÔNG điều khiển. */\n defaultValue?: string;\n /** Mỗi lần nội dung đổi, nhận markdown đã chuẩn hoá. */\n onChange?: (markdown: string) => void;\n /**\n * ⌘/Ctrl+Enter (ngoài danh sách việc — ở đó phím này tick mục). Cho ô bình luận; không truyền thì\n * phím giữ nghĩa mặc định.\n */\n onSubmit?: () => void;\n onFocus?: () => void;\n onBlur?: () => void;\n /** Chữ mờ khi rỗng. Mặc định theo `ComitorLocaleProvider`. */\n placeholder?: string;\n /** `false` = chế độ ĐỌC: không con trỏ, không menu, liên kết bấm mở được. Đổi lúc chạy được. */\n editable?: boolean;\n autoFocus?: boolean;\n /**\n * `outline` (mặc định) — khung như `Textarea`, cho biểu mẫu và ô bình luận.\n * `ghost` — không khung, không đệm: tài liệu nằm thẳng trên trang, kiểu mô tả việc của Linear.\n */\n variant?: MarkdownEditorVariant;\n /** Bật `@`. Không truyền thì gõ `@` là chữ thường — nhưng nhắc tên CÓ SẴN trong nội dung vẫn hiện đúng. */\n mentions?: MarkdownEditorMentions;\n /**\n * Bật ảnh: dán, kéo thả, và mục \"Ảnh\" của menu `/`. Nhận `File`, trả `src` để LƯU vào markdown —\n * khoá trong kho, hoặc một route ổn định của app; KHÔNG trả URL đã ký có hạn (xem `resolveImageSrc`).\n */\n onImageUpload?: (file: File) => Promise<MarkdownEditorImageUploadResult>;\n /** Lỗi tải ảnh. Mặc định: toast `imageUploadFailed` (cần `Toaster` ở gốc app). */\n onImageUploadError?: (error: unknown, file: File) => void;\n /** Đổi `src` ĐÃ LƯU thành địa chỉ HIỂN THỊ (vd URL đã ký) lúc vẽ. Markdown vẫn giữ `src` gốc. */\n resolveImageSrc?: (src: string) => string;\n /**\n * Extension Tiptap thêm vào (vd node `KEY-n` của module). Chỉ đọc LÚC MOUNT — đổi danh sách là\n * phải dựng lại trình soạn (mất lịch sử hoàn tác), nên muốn đổi thì đổi `key` của component.\n * Phải cùng phiên bản `@tiptap/*` với gói.\n */\n extensions?: readonly AnyExtension[];\n labels?: Partial<MarkdownEditorLabels>;\n /** Có `name` thì kèm một `<input type=\"hidden\">` mang markdown — để trình soạn nằm trong `<form>` thật. */\n name?: string;\n /** Đặt lên vùng soạn. Có `<label for={id}>` trong trang thì gói tự nối nó làm tên trợ năng. */\n id?: string;\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: boolean | \"true\" | \"false\";\n \"aria-required\"?: boolean | \"true\" | \"false\";\n className?: string;\n /** Lớp thêm cho vùng soạn (phần tử ProseMirror) — vd `min-h-40`, `max-h-96 overflow-y-auto`. */\n contentClassName?: string;\n ref?: React.Ref<MarkdownEditorHandle>;\n}\n\ntype MentionSelection = { id: string; label: string };\n\nconst MENTION_PLUGIN_KEY = new PluginKey(\"comitorMentionMenu\");\n\n/** Trần số người hiện khi nguồn là MẢNG mà chưa gõ gì — gõ thêm chữ là lọc; 500 dòng DOM thì không. */\nconst MENTION_LIST_LIMIT = 50;\n\nfunction filterMentionItems(items: readonly MarkdownEditorMentionItem[], query: string): MarkdownEditorMentionItem[] {\n const term = query.trim();\n const matches = term\n ? items.filter(\n (item) =>\n matchesSearch(item.label, term) ||\n // Đi QUA từ đầu của tên: \"@nguyenvana\" vẫn tới \"Nguyễn Văn An\" — cùng luật `Mention`.\n matchesSearch(item.label.replace(/\\s+/g, \"\"), term.replace(/\\s+/g, \"\")) ||\n (item.description ? matchesSearch(item.description, term) : false)\n )\n : items;\n return matches.slice(0, MENTION_LIST_LIMIT);\n}\n\nfunction readMarkdown(editor: Editor): string {\n return trimMarkdown(editor.getMarkdown());\n}\n\nconst toBoolean = (value: boolean | \"true\" | \"false\" | undefined) => value === true || value === \"true\";\n\nexport function MarkdownEditor({\n value,\n defaultValue,\n onChange,\n onSubmit,\n onFocus,\n onBlur,\n placeholder,\n editable = true,\n autoFocus = false,\n variant = \"outline\",\n mentions,\n onImageUpload,\n onImageUploadError,\n resolveImageSrc,\n extensions: extraExtensions,\n labels: labelsProp,\n name,\n id,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n className,\n contentClassName,\n ref\n}: MarkdownEditorProps) {\n const labels = useComitorLabels(\"markdownEditor\", labelsProp);\n\n /*\n * Mọi giá trị đổi được SAU KHI dựng đi qua ref: danh sách extension chỉ dựng MỘT lần (đổi nó là\n * dựng lại trình soạn — mất lịch sử hoàn tác, mất con trỏ), nên extension đọc giá trị mới nhất lúc\n * cần chứ không chép lúc dựng.\n */\n const runtime = useAsRef<MarkdownEditorRuntime>({\n placeholder: placeholder ?? labels.placeholder,\n taskCheckbox: labels.taskCheckbox,\n resolveImageSrc,\n resolveMentionLabel: mentions?.resolveLabel\n });\n const latest = useAsRef({ onChange, onFocus, onBlur, mentions, onImageUpload, labels, name });\n const imageHandlers = useAsRef<ImageUploadHandlers>({\n upload: onImageUpload,\n onError: (error, file) => {\n if (onImageUploadError) onImageUploadError(error, file);\n else toast(labels.imageUploadFailed, { variant: \"destructive\", description: file.name || undefined });\n },\n resolveSrc: resolveImageSrc,\n uploadingLabel: labels.imageUploading\n });\n\n const [linkRequest, setLinkRequest] = React.useState(0);\n const keymapHandlers = useAsRef<EditorKeymapHandlers>({\n onLinkShortcut: () => {\n if (!editable) return false;\n setLinkRequest((count) => count + 1);\n return true;\n },\n onSubmitShortcut: () => {\n if (!onSubmit) return false;\n onSubmit();\n return true;\n }\n });\n\n const initialMarkdown = value ?? defaultValue ?? \"\";\n const lastMarkdown = React.useRef(initialMarkdown);\n const [hiddenValue, setHiddenValue] = React.useState(initialMarkdown);\n const pendingUploads = useLazyRef<PendingUploads>(() => new Set()).current;\n const fileInputRef = React.useRef<HTMLInputElement>(null);\n\n const slashStore = useLazyRef(\n () => new SuggestionStore<SlashMenuItem, SlashMenuItem>(SLASH_PLUGIN_KEY, (item) => item)\n ).current;\n const mentionStore = useLazyRef(\n () =>\n new SuggestionStore<MarkdownEditorMentionItem, MentionSelection>(MENTION_PLUGIN_KEY, (item) => ({\n id: item.id,\n label: item.label\n }))\n ).current;\n\n const [extensions] = React.useState<AnyExtension[]>(() => [\n ...createMarkdownSchemaExtensions({\n runtime,\n mentionSuggestion: {\n pluginKey: MENTION_PLUGIN_KEY,\n char: \"@\",\n // Tên người Việt có dấu cách. Khi chữ gõ có dấu cách mà không còn ai khớp, menu tự thoát\n // (`exitWhenEmpty`) — người dùng đang viết câu, không còn tìm người nữa.\n allowSpaces: true,\n debounce: typeof mentions?.items === \"function\" ? 150 : 0,\n items: async ({ query, signal }) => {\n const source = latest.current.mentions?.items;\n if (!source) return [];\n if (typeof source === \"function\") return [...(await source(query, { signal }))];\n return filterMentionItems(source, query);\n },\n allow: ({ editor: current }) =>\n Boolean(latest.current.mentions) &&\n current.isEditable &&\n !current.isActive(\"codeBlock\") &&\n !current.isActive(\"code\"),\n render: mentionStore.render\n }\n }),\n createSlashCommandExtension({\n items: ({ editor: current, query }) =>\n filterSlashCommands(current, query, latest.current.labels, {\n pickImage: latest.current.onImageUpload ? () => fileInputRef.current?.click() : undefined\n }),\n render: slashStore.render\n }),\n createImageUploadExtension(imageHandlers, pendingUploads),\n createEditorKeymap(keymapHandlers),\n ...(extraExtensions ?? [])\n ]);\n\n /*\n * `<label for={id}>` không trỏ được vào một `div contenteditable` (chỉ `input`/`textarea`/… là\n * \"labelable\") — nên `FormField` dựng nhãn đúng mà vùng soạn vẫn không có tên, và bấm nhãn không\n * đưa con trỏ vào. Effect bên dưới tìm nhãn theo `for` và ghi id của nó vào đây; tên trợ năng đi\n * qua `aria-labelledby` trong bộ thuộc tính — nhờ vậy tên dự phòng (`editorLabel`) được BỎ hẳn\n * chứ không nằm song song với nhãn thật.\n */\n const [labelElementId, setLabelElementId] = React.useState<string | null>(null);\n\n /*\n * Thuộc tính của vùng soạn. Ổn định theo giá trị: `useEditor` so tuỳ chọn mỗi lượt render và gửi\n * lại `editorProps` mỗi khi định danh đổi — tức mỗi lần FormField bật/tắt `aria-invalid` là một\n * lần cập nhật đúng, chứ không phải mỗi phím gõ.\n *\n * Tên trợ năng: chỉ đặt khi SOẠN được (`role=\"textbox\"`). Ở chế độ đọc, vùng nội dung là văn bản\n * thường; `aria-label` trên một `div` không vai trò là thứ ARIA 1.2 cấm và trình đọc màn hình bỏ qua.\n * ⚠ Chế độ đọc phải GHI ĐÈ `role` chứ không bỏ trống: Tiptap luôn trộn `role=\"textbox\"` mặc định\n * vào TRƯỚC thuộc tính của app (`getEditorViewAttributes`), nên thiếu khoá là danh sách bình luận\n * đọc lên thành một dãy \"ô nhập chỉ đọc\" — đã gặp ở harness.\n */\n const editorProps = React.useMemo(() => {\n const attributes: Record<string, string> = {\n class: cn(\n MARKDOWN_EDITOR_CONTENT_CLASS,\n variant === \"outline\" ? \"min-h-16 px-3 py-2\" : \"min-h-6\",\n contentClassName\n )\n };\n if (id) attributes.id = id;\n if (!editable) attributes.role = \"none\";\n if (editable) {\n attributes.role = \"textbox\";\n attributes[\"aria-multiline\"] = \"true\";\n const labelledBy = ariaLabelledBy ?? labelElementId;\n if (ariaLabel || !labelledBy) attributes[\"aria-label\"] = ariaLabel ?? labels.editorLabel;\n if (labelledBy) attributes[\"aria-labelledby\"] = labelledBy;\n if (toBoolean(ariaRequired)) attributes[\"aria-required\"] = \"true\";\n }\n if (ariaDescribedBy) attributes[\"aria-describedby\"] = ariaDescribedBy;\n if (toBoolean(ariaInvalid)) attributes[\"aria-invalid\"] = \"true\";\n return { attributes };\n }, [\n variant,\n contentClassName,\n id,\n editable,\n ariaLabel,\n ariaLabelledBy,\n labelElementId,\n ariaDescribedBy,\n ariaInvalid,\n ariaRequired,\n labels.editorLabel\n ]);\n\n const editor = useEditor({\n extensions,\n content: initialMarkdown,\n contentType: \"markdown\",\n editable,\n autofocus: autoFocus ? \"end\" : false,\n // Next App Router render ở máy chủ: dựng trình soạn ở client sau lần hydrate đầu, không thì HTML\n // hai phía lệch nhau. Khung rỗng giữ chỗ trong khoảng đó — xem phần render.\n immediatelyRender: false,\n editorProps,\n onUpdate: ({ editor: current }) => {\n const markdown = readMarkdown(current);\n lastMarkdown.current = markdown;\n latest.current.onChange?.(markdown);\n if (latest.current.name) setHiddenValue(markdown);\n },\n onFocus: () => latest.current.onFocus?.(),\n onBlur: () => latest.current.onBlur?.()\n });\n\n // Giá trị đổi từ NGOÀI (xoá sau khi gửi, nạp bản nháp…). Giá trị chính mình vừa phát thì bỏ qua —\n // không thì mỗi phím gõ là một lần nạp lại, con trỏ nhảy về đầu.\n React.useEffect(() => {\n if (!editor || value === undefined || value === lastMarkdown.current) return;\n editor.commands.setContent(value, { contentType: \"markdown\", emitUpdate: false });\n lastMarkdown.current = value;\n setHiddenValue(value);\n }, [editor, value]);\n\n // `useEditor` cố ý giữ nguyên `editable` khi cập nhật tuỳ chọn, nên phải tự đặt. Không phát `update`:\n // đổi chế độ không phải đổi nội dung.\n React.useEffect(() => {\n if (editor && editor.isEditable !== editable) editor.setEditable(editable, false);\n }, [editor, editable]);\n\n // Nối `<label for={id}>` — lý do ở chỗ khai `labelElementId`. Bấm nhãn thì tự đưa con trỏ vào.\n React.useEffect(() => {\n if (!editor || !id || !editable || ariaLabel || ariaLabelledBy) return;\n const label = document.querySelector<HTMLLabelElement>(`label[for=\"${CSS.escape(id)}\"]`);\n if (!label) return;\n if (!label.id) label.id = `${id}-label`;\n setLabelElementId(label.id);\n const focusEditor = (event: MouseEvent) => {\n event.preventDefault();\n editor.commands.focus();\n };\n label.addEventListener(\"click\", focusEditor);\n return () => {\n label.removeEventListener(\"click\", focusEditor);\n setLabelElementId(null);\n };\n }, [editor, id, editable, ariaLabel, ariaLabelledBy]);\n\n /*\n * Handle ỔN ĐỊNH suốt đời component (deps rỗng), đọc trình soạn MỚI NHẤT qua ref. Dựng lại theo\n * `editor` thì ai giữ đối tượng cũ — một closure, một biến ngoài React — thấy `editor: null` mãi,\n * vì trình soạn chỉ có SAU lượt render đầu (`immediatelyRender: false`). Đã gặp ở harness.\n */\n const editorRef = useAsRef(editor);\n React.useImperativeHandle(\n ref,\n () => ({\n focus: (position = \"end\") => {\n editorRef.current?.commands.focus(position);\n },\n getMarkdown: () => (editorRef.current ? readMarkdown(editorRef.current) : lastMarkdown.current),\n setMarkdown: (markdown) => {\n editorRef.current?.commands.setContent(markdown, { contentType: \"markdown\", emitUpdate: true });\n },\n clear: () => {\n editorRef.current?.commands.clearContent(true);\n },\n isEmpty: () => (editorRef.current ? editorRef.current.isEmpty : lastMarkdown.current.trim() === \"\"),\n isUploading: () => pendingUploads.size > 0,\n get editor() {\n return editorRef.current;\n }\n }),\n [editorRef, pendingUploads]\n );\n\n return (\n <div\n data-slot=\"markdown-editor\"\n data-variant={variant}\n data-readonly={!editable || undefined}\n className={cn(\n \"relative min-w-0\",\n variant === \"outline\" &&\n // Khuôn khung của `Textarea`: viền `border-input`, nền trong suốt (dark: `--input-fill`),\n // focus `--primary-ink` + vòng `ring/50`; lỗi đọc từ `aria-invalid` của CHÍNH vùng soạn.\n \"rounded-md border border-input bg-transparent shadow-xs transition-colors duration-150 ease-out focus-within:border-primary-ink focus-within:ring-2 focus-within:ring-ring/50 has-[[aria-invalid=true]]:border-destructive-ink dark:bg-input-fill\",\n className\n )}\n >\n {editor ? (\n <EditorContent editor={editor} />\n ) : (\n // Giữ chỗ tới khi trình soạn dựng xong ở client (`immediatelyRender: false`) — cùng chiều cao tối thiểu.\n <div aria-hidden=\"true\" className={variant === \"outline\" ? \"min-h-16 px-3 py-2\" : \"min-h-6\"} />\n )}\n\n {editor && editable && <BubbleToolbar editor={editor} labels={labels} linkRequest={linkRequest} />}\n\n {editor && (\n <SuggestionMenu\n editor={editor}\n store={slashStore}\n label={labels.slashMenu}\n emptyLabel={labels.slashEmpty}\n getKey={(item) => item.id}\n renderItem={(item) => (\n <>\n <item.icon aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.hint && (\n // Cú pháp gõ tắt — mờ, chữ đều, như phím tắt trong `CommandShortcut`.\n <span className=\"ml-auto font-mono text-xs text-muted-foreground\">{item.hint}</span>\n )}\n </>\n )}\n />\n )}\n\n {editor && mentions && (\n <SuggestionMenu\n editor={editor}\n store={mentionStore}\n label={labels.mentionMenu}\n emptyLabel={labels.mentionEmpty}\n loadingLabel={labels.mentionLoading}\n getKey={(item) => item.id}\n exitWhenEmpty={(query) => /\\s/.test(query)}\n renderItem={(item) => (\n <>\n <LetterAvatar name={item.label} src={item.avatarUrl} size=\"xs\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.description && (\n <span className=\"max-w-[45%] truncate text-xs text-muted-foreground\">{item.description}</span>\n )}\n </>\n )}\n />\n )}\n\n {onImageUpload && (\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n hidden\n tabIndex={-1}\n onChange={(event) => {\n const files = Array.from(event.target.files ?? []).filter((file) => file.type.startsWith(\"image/\"));\n // Xoá giá trị để chọn LẠI đúng tệp đó lần sau vẫn phát `change`.\n event.target.value = \"\";\n if (editor && files.length > 0)\n insertImageFiles(editor, files, undefined, imageHandlers.current, pendingUploads);\n }}\n />\n )}\n\n {name && <input type=\"hidden\" name={name} value={hiddenValue} />}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/editor/markdown-editor.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * MarkdownEditor — trình soạn văn bản giàu định dạng kiểu Linear, LƯU bằng markdown.\n *\n * Soạn trực tiếp trên bản đã định dạng (không có khung \"Xem trước\" tách đôi): gõ `## ` là thành\n * tiêu đề, `- ` là danh sách, `[] ` là danh sách việc, ```` ``` ```` là khối mã; `/` mở menu khối,\n * `@` mở danh sách người, bôi chọn chữ là hiện thanh định dạng. Giá trị vào/ra là CHUỖI markdown\n * (GFM) — thứ module lưu ở cột mô tả và thứ công cụ MCP đọc/ghi.\n *\n * Cần lưu HTML (trang CMS, nội dung hiển thị lại ngoài trình soạn)? Đó là `RichTextEditor` — cùng\n * lõi (`editor-core.tsx`), lược đồ rộng hơn. Chọn theo thứ CỘT LƯU mang, không theo giao diện: cả hai\n * đều có `toolbar=\"bubble\" | \"fixed\"`.\n *\n * Dựng trên Tiptap 3 (ProseMirror — cùng nền với trình soạn của Linear). Lý do chọn, các phương án\n * đã loại, và những chỗ đã vá `@tiptap/markdown`: docs/editor.md.\n *\n * ⚠ Mọi gói `@tiptap/*` là peer TUỲ CHỌN của entry `@comitor/ui/editor` và phải CÙNG MỘT phiên bản\n * (chúng ghim nhau bằng số chính xác). Lệnh cài ở `src/editor/index.ts`.\n */\n\nimport type * as React from \"react\";\nimport { EditorCore, type EditorCoreHandle } from \"./editor-core.js\";\nimport type { EditorBaseProps, EditorHandleBase, EditorToolbarMode } from \"./editor-types.js\";\n\n/** Điều khiển trình soạn từ ngoài — lấy qua prop `ref`. */\nexport interface MarkdownEditorHandle extends EditorHandleBase {\n /** Markdown hiện tại — cùng chuỗi mà `onChange` nhận. */\n getMarkdown: () => string;\n /** Thay toàn bộ nội dung. Có gọi `onChange`. */\n setMarkdown: (markdown: string) => void;\n}\n\nexport interface MarkdownEditorProps extends EditorBaseProps {\n /** Markdown — điều khiển từ ngoài. Đổi giá trị KHÁC thứ trình soạn vừa phát là nạp lại nội dung. */\n value?: string;\n /** Markdown ban đầu khi KHÔNG điều khiển. */\n defaultValue?: string;\n /** Mỗi lần nội dung đổi, nhận markdown đã chuẩn hoá. */\n onChange?: (markdown: string) => void;\n /**\n * `bubble` (mặc định) — kiểu Linear: định dạng hiện ở chỗ bôi chọn. `fixed` — thanh dính đầu khung,\n * cho người soạn không thuộc phím tắt (mô tả dài, trang nội bộ). Xem `EditorToolbarMode`.\n */\n toolbar?: EditorToolbarMode;\n ref?: React.Ref<MarkdownEditorHandle>;\n}\n\nfunction createMarkdownHandle(core: EditorCoreHandle): MarkdownEditorHandle {\n return {\n focus: core.focus,\n getMarkdown: core.getValue,\n setMarkdown: core.setValue,\n clear: core.clear,\n isEmpty: core.isEmpty,\n isUploading: core.isUploading,\n get editor() {\n return core.editor;\n }\n };\n}\n\nexport function MarkdownEditor({ toolbar = \"bubble\", ref, ...props }: MarkdownEditorProps) {\n return (\n <EditorCore\n {...props}\n format=\"markdown\"\n toolbar={toolbar}\n slot=\"markdown-editor\"\n handleRef={ref}\n createHandle={createMarkdownHandle}\n />\n );\n}\n"],"mappings":";AAgEI;AA1CJ,SAAS,kBAAyC;AA0BlD,SAAS,qBAAqB,MAA8C;AAC1E,SAAO;AAAA,IACL,OAAO,KAAK;AAAA,IACZ,aAAa,KAAK;AAAA,IAClB,aAAa,KAAK;AAAA,IAClB,OAAO,KAAK;AAAA,IACZ,SAAS,KAAK;AAAA,IACd,aAAa,KAAK;AAAA,IAClB,IAAI,SAAS;AACX,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACF;AAEO,SAAS,eAAe,EAAE,UAAU,UAAU,KAAK,GAAG,MAAM,GAAwB;AACzF,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,QAAO;AAAA,MACP;AAAA,MACA,MAAK;AAAA,MACL,WAAW;AAAA,MACX,cAAc;AAAA;AAAA,EAChB;AAEJ;","names":[]}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { Extension, AnyExtension } from '@tiptap/core';
|
|
2
2
|
import { MentionOptions } from '@tiptap/extension-mention';
|
|
3
|
+
import { Slice } from '@tiptap/pm/model';
|
|
4
|
+
import { f as EditorFormat } from '../editor-types-BMST7-VR.js';
|
|
5
|
+
import './markdown-editor-constants.js';
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
|
-
* LƯỢC ĐỒ
|
|
8
|
+
* LƯỢC ĐỒ của hai trình soạn — danh sách extension Tiptap, không React, không DOM.
|
|
6
9
|
*
|
|
7
|
-
* Mọi thứ quyết định "tài liệu này lưu xuống thành chuỗi
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* Mọi thứ quyết định "tài liệu này lưu xuống thành chuỗi nào, và đọc lên lại thành tài liệu nào"
|
|
11
|
+
* nằm ở đây, và CHỈ ở đây: trình soạn dùng nó, cổng `test:editor-markdown` dựng `MarkdownManager`
|
|
12
|
+
* từ chính hàm này trên `dist/` rồi đo vòng tài liệu → markdown → tài liệu. Hai nơi mà dùng hai danh
|
|
13
|
+
* sách là hai lược đồ, và cổng xanh trên một lược đồ không ai dùng.
|
|
14
|
+
*
|
|
15
|
+
* Hai định dạng LƯU, hai lược đồ khác nhau ở đúng những chỗ định dạng ấy biểu diễn được hay không
|
|
16
|
+
* (`createSchemaExtensions`): markdown (`MarkdownEditor`) hẹp hơn — không gạch chân, ô bảng một đoạn —
|
|
17
|
+
* vì thứ gì không ghi được thành markdown thì sẽ mất ở lần lưu; HTML (`RichTextEditor`) giữ được cả hai.
|
|
11
18
|
*
|
|
12
19
|
* Phần GIAO DIỆN (menu `/`, menu `@`, tải ảnh, phím tắt) gắn vào qua tham số — xem
|
|
13
|
-
* `MarkdownSchemaOptions`. Gọi không tham số là ra đúng lược đồ, đủ để đọc/ghi
|
|
20
|
+
* `MarkdownSchemaOptions`. Gọi không tham số là ra đúng lược đồ, đủ để đọc/ghi.
|
|
14
21
|
*/
|
|
15
22
|
|
|
16
23
|
/**
|
|
@@ -27,7 +34,9 @@ interface MarkdownEditorRuntime {
|
|
|
27
34
|
resolveMentionLabel?: (id: string) => string | undefined;
|
|
28
35
|
}
|
|
29
36
|
interface MarkdownSchemaOptions {
|
|
30
|
-
/**
|
|
37
|
+
/** Định dạng LƯU — quyết định lược đồ. Mặc định `markdown`. */
|
|
38
|
+
format?: EditorFormat;
|
|
39
|
+
/** Đọc lười — xem `MarkdownEditorRuntime`. Bỏ trống ở nơi chỉ cần đọc/ghi (cổng, máy chủ). */
|
|
31
40
|
runtime?: {
|
|
32
41
|
readonly current: MarkdownEditorRuntime;
|
|
33
42
|
};
|
|
@@ -39,14 +48,30 @@ interface MarkdownSchemaOptions {
|
|
|
39
48
|
mentionSuggestion?: Partial<MentionOptions["suggestion"]>;
|
|
40
49
|
}
|
|
41
50
|
/**
|
|
42
|
-
*
|
|
51
|
+
* Gộp một đoạn dán thành MỘT dòng nội dòng: các khối chữ nối nhau bằng một dấu cách, xuống dòng (cả
|
|
52
|
+
* `hardBreak` lẫn `\n` trong khối mã) thành dấu cách, khối không phải chữ (ảnh, đường kẻ) bỏ đi.
|
|
53
|
+
* Nội dung nội dòng sẵn thì để nguyên; `keepTables` (đoạn dán GỐC có bảng) thì đoạn dán mang ô bảng
|
|
54
|
+
* cũng để nguyên — dán theo ô là việc của `prosemirror-tables`.
|
|
55
|
+
* Xuất ra cho cổng `test:editor-markdown`; không nằm trong entry.
|
|
56
|
+
*/
|
|
57
|
+
declare function flattenSliceToLine(slice: Slice, keepTables?: boolean): Slice;
|
|
58
|
+
/**
|
|
59
|
+
* Danh sách extension tạo nên lược đồ của trình soạn, theo định dạng LƯU.
|
|
60
|
+
*
|
|
61
|
+
* Markdown (`format: "markdown"`, mặc định) hẹp hơn HTML ở hai chỗ, cả hai vì cùng một lý do — thứ gì
|
|
62
|
+
* không ghi được thành markdown thì MẤT ở lần lưu, mà người dùng không được báo:
|
|
63
|
+
* · `underline` tắt: markdown không có cú pháp gạch chân. `@tiptap/markdown` ghi nó thành
|
|
64
|
+
* `++chữ++` — cú pháp riêng, nên email thông báo, tác tử đọc qua MCP và mọi trình xem markdown
|
|
65
|
+
* khác in nguyên hai dấu cộng;
|
|
66
|
+
* · ô bảng chỉ chứa MỘT đoạn văn (`ComitorTableCell`).
|
|
67
|
+
* Hai bản vá markdown (`ComitorParagraph`, `Markdown` với `Marked` riêng) cũng chỉ nạp ở định dạng đó.
|
|
43
68
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* in nguyên hai dấu cộng. Một định dạng chỉ sống trong trình soạn này thì không nên tồn tại trong
|
|
47
|
-
* định dạng LƯU.
|
|
69
|
+
* HTML (`format: "html"`) giữ cả gạch chân lẫn ô bảng nhiều khối — `<u>`, `<td><ul>…</ul></td>` là HTML
|
|
70
|
+
* hợp lệ và đọc lên lại đúng.
|
|
48
71
|
*/
|
|
49
|
-
declare function
|
|
72
|
+
declare function createSchemaExtensions(options?: MarkdownSchemaOptions): AnyExtension[];
|
|
73
|
+
/** Lược đồ của `MarkdownEditor` — tên có từ `3.5.0`, cổng `test:editor-markdown` gọi thẳng nó. */
|
|
74
|
+
declare function createMarkdownSchemaExtensions(options?: Omit<MarkdownSchemaOptions, "format">): AnyExtension[];
|
|
50
75
|
/** Phím tắt riêng của trình soạn — xem `createEditorKeymap`. */
|
|
51
76
|
interface EditorKeymapHandlers {
|
|
52
77
|
/** ⌘/Ctrl+K — mở ô liên kết. Trả `true` khi đã xử lý. */
|
|
@@ -66,6 +91,6 @@ interface EditorKeymapHandlers {
|
|
|
66
91
|
*/
|
|
67
92
|
declare function createEditorKeymap(handlers: {
|
|
68
93
|
readonly current: EditorKeymapHandlers;
|
|
69
|
-
}): Extension;
|
|
94
|
+
}, format?: EditorFormat): Extension;
|
|
70
95
|
|
|
71
|
-
export { type EditorKeymapHandlers, type MarkdownEditorRuntime, type MarkdownSchemaOptions, createEditorKeymap, createMarkdownSchemaExtensions };
|
|
96
|
+
export { type EditorKeymapHandlers, type MarkdownEditorRuntime, type MarkdownSchemaOptions, createEditorKeymap, createMarkdownSchemaExtensions, createSchemaExtensions, flattenSliceToLine };
|