@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.
Files changed (51) hide show
  1. package/README.md +4 -2
  2. package/dist/data-grid/data-grid-cell-variants.js +6 -1
  3. package/dist/data-grid/data-grid-cell-variants.js.map +1 -1
  4. package/dist/data-grid/types.d.ts +10 -0
  5. package/dist/data-grid/use-data-grid.d.ts +2 -2
  6. package/dist/data-grid/use-data-grid.js +30 -13
  7. package/dist/data-grid/use-data-grid.js.map +1 -1
  8. package/dist/editor/bubble-toolbar.d.ts +20 -1
  9. package/dist/editor/bubble-toolbar.js +12 -172
  10. package/dist/editor/bubble-toolbar.js.map +1 -1
  11. package/dist/editor/editor-core.d.ts +28 -0
  12. package/dist/editor/editor-core.js +354 -0
  13. package/dist/editor/editor-core.js.map +1 -0
  14. package/dist/editor/editor-types.d.ts +3 -0
  15. package/dist/editor/editor-types.js +1 -0
  16. package/dist/editor/editor-types.js.map +1 -0
  17. package/dist/editor/fixed-toolbar.d.ts +19 -0
  18. package/dist/editor/fixed-toolbar.js +106 -0
  19. package/dist/editor/fixed-toolbar.js.map +1 -0
  20. package/dist/editor/image-upload.d.ts +3 -57
  21. package/dist/editor/image-upload.js +3 -2
  22. package/dist/editor/image-upload.js.map +1 -1
  23. package/dist/editor/index.d.ts +3 -2
  24. package/dist/editor/index.js +4 -4
  25. package/dist/editor/index.js.map +1 -1
  26. package/dist/editor/markdown-editor-classes.js +6 -4
  27. package/dist/editor/markdown-editor-classes.js.map +1 -1
  28. package/dist/editor/markdown-editor-constants.d.ts +8 -2
  29. package/dist/editor/markdown-editor-constants.js.map +1 -1
  30. package/dist/editor/markdown-editor.d.ts +14 -91
  31. package/dist/editor/markdown-editor.js +23 -315
  32. package/dist/editor/markdown-editor.js.map +1 -1
  33. package/dist/editor/markdown-extensions.d.ts +40 -15
  34. package/dist/editor/markdown-extensions.js +143 -24
  35. package/dist/editor/markdown-extensions.js.map +1 -1
  36. package/dist/editor/rich-text-editor.d.ts +48 -0
  37. package/dist/editor/rich-text-editor.js +33 -0
  38. package/dist/editor/rich-text-editor.js.map +1 -0
  39. package/dist/editor/slash-commands.d.ts +4 -1
  40. package/dist/editor/slash-commands.js +7 -2
  41. package/dist/editor/slash-commands.js.map +1 -1
  42. package/dist/editor/toolbar-actions.d.ts +44 -0
  43. package/dist/editor/toolbar-actions.js +173 -0
  44. package/dist/editor/toolbar-actions.js.map +1 -0
  45. package/dist/editor/toolbar-button.d.ts +15 -0
  46. package/dist/editor/toolbar-button.js +47 -0
  47. package/dist/editor/toolbar-button.js.map +1 -0
  48. package/dist/editor-types-BMST7-VR.d.ts +171 -0
  49. package/dist/i18n/namespaces/markdown-editor.js +6 -0
  50. package/dist/i18n/namespaces/markdown-editor.js.map +1 -1
  51. package/package.json +1 -1
@@ -0,0 +1,354 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { PluginKey } from "@tiptap/pm/state";
4
+ import { EditorContent, useEditor } from "@tiptap/react";
5
+ import * as React from "react";
6
+ import { LetterAvatar } from "../components/composite/letter-avatar.js";
7
+ import { toast } from "../components/composite/toast.js";
8
+ import { useComitorLabels } from "../i18n/locale-provider.js";
9
+ import { cn } from "../lib/cn.js";
10
+ import { matchesSearch } from "../lib/text-search.js";
11
+ import { useAsRef } from "../lib/use-as-ref.js";
12
+ import { useLazyRef } from "../lib/use-lazy-ref.js";
13
+ import { BubbleToolbar } from "./bubble-toolbar.js";
14
+ import { FixedToolbar } from "./fixed-toolbar.js";
15
+ import {
16
+ createImageUploadExtension,
17
+ insertImageFiles
18
+ } from "./image-upload.js";
19
+ import { MARKDOWN_EDITOR_CONTENT_CLASS } from "./markdown-editor-classes.js";
20
+ import {
21
+ createEditorKeymap,
22
+ createSchemaExtensions
23
+ } from "./markdown-extensions.js";
24
+ import { trimMarkdown } from "./markdown-syntax.js";
25
+ import {
26
+ createSlashCommandExtension,
27
+ filterSlashCommands,
28
+ SLASH_PLUGIN_KEY
29
+ } from "./slash-commands.js";
30
+ import { SuggestionMenu } from "./suggestion-menu.js";
31
+ import { SuggestionStore } from "./suggestion-store.js";
32
+ const MENTION_PLUGIN_KEY = new PluginKey("comitorMentionMenu");
33
+ const MENTION_LIST_LIMIT = 50;
34
+ function filterMentionItems(items, query) {
35
+ const term = query.trim();
36
+ const matches = term ? items.filter(
37
+ (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`.
38
+ matchesSearch(item.label.replace(/\s+/g, ""), term.replace(/\s+/g, "")) || (item.description ? matchesSearch(item.description, term) : false)
39
+ ) : items;
40
+ return matches.slice(0, MENTION_LIST_LIMIT);
41
+ }
42
+ function readValue(editor, format) {
43
+ if (format === "markdown") return trimMarkdown(editor.getMarkdown());
44
+ return editor.isEmpty ? "" : editor.getHTML();
45
+ }
46
+ const toBoolean = (value) => value === true || value === "true";
47
+ function EditorCore({
48
+ format,
49
+ toolbar,
50
+ value,
51
+ defaultValue,
52
+ onChange,
53
+ onSubmit,
54
+ onFocus,
55
+ onBlur,
56
+ placeholder,
57
+ editable = true,
58
+ autoFocus = false,
59
+ variant = "outline",
60
+ mentions,
61
+ onImageUpload,
62
+ onImageUploadError,
63
+ resolveImageSrc,
64
+ extensions: extraExtensions,
65
+ labels: labelsProp,
66
+ name,
67
+ id,
68
+ "aria-label": ariaLabel,
69
+ "aria-labelledby": ariaLabelledBy,
70
+ "aria-describedby": ariaDescribedBy,
71
+ "aria-invalid": ariaInvalid,
72
+ "aria-required": ariaRequired,
73
+ className,
74
+ contentClassName,
75
+ slot,
76
+ handleRef,
77
+ createHandle
78
+ }) {
79
+ const labels = useComitorLabels("markdownEditor", labelsProp);
80
+ const runtime = useAsRef({
81
+ placeholder: placeholder ?? labels.placeholder,
82
+ taskCheckbox: labels.taskCheckbox,
83
+ resolveImageSrc,
84
+ resolveMentionLabel: mentions?.resolveLabel
85
+ });
86
+ const latest = useAsRef({ onChange, onFocus, onBlur, mentions, onImageUpload, labels, name });
87
+ const imageHandlers = useAsRef({
88
+ upload: onImageUpload,
89
+ onError: (error, file) => {
90
+ if (onImageUploadError) onImageUploadError(error, file);
91
+ else toast(labels.imageUploadFailed, { variant: "destructive", description: file.name || void 0 });
92
+ },
93
+ resolveSrc: resolveImageSrc,
94
+ uploadingLabel: labels.imageUploading,
95
+ format
96
+ });
97
+ const [linkRequest, setLinkRequest] = React.useState(0);
98
+ const requestLink = React.useCallback(() => setLinkRequest((count) => count + 1), []);
99
+ const keymapHandlers = useAsRef({
100
+ onLinkShortcut: () => {
101
+ if (!editable) return false;
102
+ requestLink();
103
+ return true;
104
+ },
105
+ onSubmitShortcut: () => {
106
+ if (!onSubmit) return false;
107
+ onSubmit();
108
+ return true;
109
+ }
110
+ });
111
+ const initialValue = value ?? defaultValue ?? "";
112
+ const lastValue = React.useRef(initialValue);
113
+ const [hiddenValue, setHiddenValue] = React.useState(initialValue);
114
+ const pendingUploads = useLazyRef(() => /* @__PURE__ */ new Set()).current;
115
+ const fileInputRef = React.useRef(null);
116
+ const pickImage = React.useCallback(() => fileInputRef.current?.click(), []);
117
+ const slashStore = useLazyRef(
118
+ () => new SuggestionStore(SLASH_PLUGIN_KEY, (item) => item)
119
+ ).current;
120
+ const mentionStore = useLazyRef(
121
+ () => new SuggestionStore(MENTION_PLUGIN_KEY, (item) => ({
122
+ id: item.id,
123
+ label: item.label
124
+ }))
125
+ ).current;
126
+ const [extensions] = React.useState(() => [
127
+ ...createSchemaExtensions({
128
+ format,
129
+ runtime,
130
+ mentionSuggestion: {
131
+ pluginKey: MENTION_PLUGIN_KEY,
132
+ char: "@",
133
+ // 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
134
+ // (`exitWhenEmpty`) — người dùng đang viết câu, không còn tìm người nữa.
135
+ allowSpaces: true,
136
+ debounce: typeof mentions?.items === "function" ? 150 : 0,
137
+ items: async ({ query, signal }) => {
138
+ const source = latest.current.mentions?.items;
139
+ if (!source) return [];
140
+ if (typeof source === "function") return [...await source(query, { signal })];
141
+ return filterMentionItems(source, query);
142
+ },
143
+ allow: ({ editor: current }) => Boolean(latest.current.mentions) && current.isEditable && !current.isActive("codeBlock") && !current.isActive("code"),
144
+ render: mentionStore.render
145
+ }
146
+ }),
147
+ createSlashCommandExtension({
148
+ items: ({ editor: current, query }) => filterSlashCommands(current, query, latest.current.labels, {
149
+ format,
150
+ pickImage: latest.current.onImageUpload ? () => fileInputRef.current?.click() : void 0
151
+ }),
152
+ render: slashStore.render
153
+ }),
154
+ createImageUploadExtension(imageHandlers, pendingUploads),
155
+ createEditorKeymap(keymapHandlers, format),
156
+ ...extraExtensions ?? []
157
+ ]);
158
+ const [labelElementId, setLabelElementId] = React.useState(null);
159
+ const contentPadding = variant === "outline" ? "min-h-16 px-3 py-2" : "min-h-6";
160
+ const editorProps = React.useMemo(() => {
161
+ const attributes = {
162
+ class: cn(MARKDOWN_EDITOR_CONTENT_CLASS, contentPadding, contentClassName)
163
+ };
164
+ if (id) attributes.id = id;
165
+ if (!editable) attributes.role = "none";
166
+ if (editable) {
167
+ attributes.role = "textbox";
168
+ attributes["aria-multiline"] = "true";
169
+ const labelledBy = ariaLabelledBy ?? labelElementId;
170
+ if (ariaLabel || !labelledBy) attributes["aria-label"] = ariaLabel ?? labels.editorLabel;
171
+ if (labelledBy) attributes["aria-labelledby"] = labelledBy;
172
+ if (toBoolean(ariaRequired)) attributes["aria-required"] = "true";
173
+ }
174
+ if (ariaDescribedBy) attributes["aria-describedby"] = ariaDescribedBy;
175
+ if (toBoolean(ariaInvalid)) attributes["aria-invalid"] = "true";
176
+ return { attributes };
177
+ }, [
178
+ contentPadding,
179
+ contentClassName,
180
+ id,
181
+ editable,
182
+ ariaLabel,
183
+ ariaLabelledBy,
184
+ labelElementId,
185
+ ariaDescribedBy,
186
+ ariaInvalid,
187
+ ariaRequired,
188
+ labels.editorLabel
189
+ ]);
190
+ const editor = useEditor({
191
+ extensions,
192
+ content: initialValue,
193
+ contentType: format,
194
+ editable,
195
+ autofocus: autoFocus ? "end" : false,
196
+ // Next App Router render ở máy chủ: dựng trình soạn ở client sau lần hydrate đầu, không thì HTML
197
+ // hai phía lệch nhau. Khung rỗng giữ chỗ trong khoảng đó — xem phần render.
198
+ immediatelyRender: false,
199
+ editorProps,
200
+ onUpdate: ({ editor: current }) => {
201
+ const next = readValue(current, format);
202
+ lastValue.current = next;
203
+ latest.current.onChange?.(next);
204
+ if (latest.current.name) setHiddenValue(next);
205
+ },
206
+ onFocus: () => latest.current.onFocus?.(),
207
+ onBlur: () => latest.current.onBlur?.()
208
+ });
209
+ React.useEffect(() => {
210
+ if (!editor || value === void 0 || value === lastValue.current) return;
211
+ editor.commands.setContent(value, { contentType: format, emitUpdate: false });
212
+ lastValue.current = value;
213
+ setHiddenValue(value);
214
+ }, [editor, value, format]);
215
+ React.useEffect(() => {
216
+ if (editor && editor.isEditable !== editable) editor.setEditable(editable, false);
217
+ }, [editor, editable]);
218
+ React.useEffect(() => {
219
+ if (!editor || !id || !editable || ariaLabel || ariaLabelledBy) return;
220
+ const label = document.querySelector(`label[for="${CSS.escape(id)}"]`);
221
+ if (!label) return;
222
+ if (!label.id) label.id = `${id}-label`;
223
+ setLabelElementId(label.id);
224
+ const focusEditor = (event) => {
225
+ event.preventDefault();
226
+ editor.commands.focus();
227
+ };
228
+ label.addEventListener("click", focusEditor);
229
+ return () => {
230
+ label.removeEventListener("click", focusEditor);
231
+ setLabelElementId(null);
232
+ };
233
+ }, [editor, id, editable, ariaLabel, ariaLabelledBy]);
234
+ const editorRef = useAsRef(editor);
235
+ const core = React.useMemo(
236
+ () => ({
237
+ focus: (position = "end") => {
238
+ editorRef.current?.commands.focus(position);
239
+ },
240
+ getValue: () => editorRef.current ? readValue(editorRef.current, format) : lastValue.current,
241
+ setValue: (next) => {
242
+ editorRef.current?.commands.setContent(next, { contentType: format, emitUpdate: true });
243
+ },
244
+ clear: () => {
245
+ editorRef.current?.commands.clearContent(true);
246
+ },
247
+ isEmpty: () => editorRef.current ? editorRef.current.isEmpty : lastValue.current.trim() === "",
248
+ isUploading: () => pendingUploads.size > 0,
249
+ get editor() {
250
+ return editorRef.current;
251
+ }
252
+ }),
253
+ [editorRef, pendingUploads, format]
254
+ );
255
+ React.useImperativeHandle(handleRef, () => createHandle(core), [createHandle, core]);
256
+ return /* @__PURE__ */ jsxs(
257
+ "div",
258
+ {
259
+ "data-slot": slot,
260
+ "data-variant": variant,
261
+ "data-toolbar": toolbar,
262
+ "data-readonly": !editable || void 0,
263
+ className: cn(
264
+ "relative min-w-0",
265
+ variant === "outline" && // Khuôn khung của `Textarea`: viền `border-input`, nền trong suốt (dark: `--input-fill`),
266
+ // focus `--primary-ink` + vòng `ring/50`; lỗi đọc từ `aria-invalid` của CHÍNH vùng soạn.
267
+ "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",
268
+ className
269
+ ),
270
+ children: [
271
+ toolbar === "fixed" && editable && /* @__PURE__ */ jsx(
272
+ FixedToolbar,
273
+ {
274
+ editor,
275
+ labels,
276
+ format,
277
+ onLink: requestLink,
278
+ onPickImage: onImageUpload ? pickImage : void 0,
279
+ className: variant === "outline" ? "rounded-t-[inherit]" : "mb-3"
280
+ }
281
+ ),
282
+ editor ? /* @__PURE__ */ jsx(EditorContent, { editor }) : (
283
+ // Giữ chỗ tới khi trình soạn dựng xong ở client (`immediatelyRender: false`) — cùng chiều cao tối thiểu.
284
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: cn(contentPadding, contentClassName) })
285
+ ),
286
+ editor && editable && /* @__PURE__ */ jsx(
287
+ BubbleToolbar,
288
+ {
289
+ editor,
290
+ labels,
291
+ format,
292
+ formatting: toolbar === "bubble",
293
+ linkRequest
294
+ }
295
+ ),
296
+ editor && /* @__PURE__ */ jsx(
297
+ SuggestionMenu,
298
+ {
299
+ editor,
300
+ store: slashStore,
301
+ label: labels.slashMenu,
302
+ emptyLabel: labels.slashEmpty,
303
+ getKey: (item) => item.id,
304
+ renderItem: (item) => /* @__PURE__ */ jsxs(Fragment, { children: [
305
+ /* @__PURE__ */ jsx(item.icon, { "aria-hidden": "true" }),
306
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
307
+ item.hint && // Cú pháp gõ tắt — mờ, chữ đều, như phím tắt trong `CommandShortcut`.
308
+ /* @__PURE__ */ jsx("span", { className: "ml-auto font-mono text-xs text-muted-foreground", children: item.hint })
309
+ ] })
310
+ }
311
+ ),
312
+ editor && mentions && /* @__PURE__ */ jsx(
313
+ SuggestionMenu,
314
+ {
315
+ editor,
316
+ store: mentionStore,
317
+ label: labels.mentionMenu,
318
+ emptyLabel: labels.mentionEmpty,
319
+ loadingLabel: labels.mentionLoading,
320
+ getKey: (item) => item.id,
321
+ exitWhenEmpty: (query) => /\s/.test(query),
322
+ renderItem: (item) => /* @__PURE__ */ jsxs(Fragment, { children: [
323
+ /* @__PURE__ */ jsx(LetterAvatar, { name: item.label, src: item.avatarUrl, size: "xs", "aria-hidden": "true" }),
324
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
325
+ item.description && /* @__PURE__ */ jsx("span", { className: "max-w-[45%] truncate text-xs text-muted-foreground", children: item.description })
326
+ ] })
327
+ }
328
+ ),
329
+ onImageUpload && /* @__PURE__ */ jsx(
330
+ "input",
331
+ {
332
+ ref: fileInputRef,
333
+ type: "file",
334
+ accept: "image/*",
335
+ multiple: true,
336
+ hidden: true,
337
+ tabIndex: -1,
338
+ onChange: (event) => {
339
+ const files = Array.from(event.target.files ?? []).filter((file) => file.type.startsWith("image/"));
340
+ event.target.value = "";
341
+ if (editor && files.length > 0)
342
+ insertImageFiles(editor, files, void 0, imageHandlers.current, pendingUploads);
343
+ }
344
+ }
345
+ ),
346
+ name && /* @__PURE__ */ jsx("input", { type: "hidden", name, value: hiddenValue })
347
+ ]
348
+ }
349
+ );
350
+ }
351
+ export {
352
+ EditorCore
353
+ };
354
+ //# sourceMappingURL=editor-core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/editor/editor-core.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Lõi CHUNG của hai trình soạn — `MarkdownEditor` (lưu markdown) và `RichTextEditor` (lưu HTML).\n *\n * Hai component công khai chỉ là hai lớp mỏng: chọn định dạng, thanh công cụ mặc định, và tên của cặp\n * đọc/ghi trên handle (`getMarkdown` / `getHTML`). Mọi thứ còn lại — menu `/` và `@`, ảnh, liên kết,\n * phím tắt, trợ năng, đồng bộ `value` — nằm ở đây, MỘT bản: hai bản là hai nơi để sửa một lỗi mà\n * quên nơi kia.\n *\n * Không xuất khỏi entry: app chọn định dạng bằng cách chọn COMPONENT, không bằng prop. Định dạng quyết\n * định cả lược đồ (`createSchemaExtensions`), mà lược đồ chỉ dựng một lần lúc mount — một prop đổi được\n * lúc chạy sẽ là một prop nói dối.\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 type {\n EditorBaseProps,\n EditorFormat,\n EditorHandleBase,\n EditorToolbarMode,\n MarkdownEditorMentionItem\n} from \"./editor-types.js\";\nimport { FixedToolbar } from \"./fixed-toolbar.js\";\nimport {\n createImageUploadExtension,\n type ImageUploadHandlers,\n insertImageFiles,\n type PendingUploads\n} from \"./image-upload.js\";\nimport { MARKDOWN_EDITOR_CONTENT_CLASS } from \"./markdown-editor-classes.js\";\nimport {\n createEditorKeymap,\n createSchemaExtensions,\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/** Handle chung, đọc/ghi theo định dạng của trình soạn — mỗi lớp ngoài đặt tên lại cho cặp này. */\nexport interface EditorCoreHandle extends EditorHandleBase {\n getValue: () => string;\n setValue: (value: string) => void;\n}\n\nexport interface EditorCoreProps<THandle> extends EditorBaseProps {\n format: EditorFormat;\n toolbar: EditorToolbarMode;\n value?: string;\n defaultValue?: string;\n onChange?: (value: string) => void;\n /** `data-slot` của khung ngoài — mỗi trình soạn một tên, để CSS/test của app nhắm đúng. */\n slot: string;\n handleRef?: React.Ref<THandle>;\n /**\n * Dựng handle công khai từ handle chung. Hàm ở cấp MODULE (định danh ổn định), và KHÔNG trải `core`\n * ra (`{ ...core }`): trải là đọc getter `editor` đúng một lần — lúc nó còn `null`.\n */\n createHandle: (core: EditorCoreHandle) => THandle;\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\n/**\n * Giá trị hiện tại theo định dạng. HTML của tài liệu rỗng là `<p></p>` — trả `\"\"` để \"rỗng\" là MỘT\n * giá trị ở cả hai trình soạn: biểu mẫu kiểm trường bắt buộc bằng `value === \"\"` mà không phải biết\n * trình soạn nào đứng sau.\n */\nfunction readValue(editor: Editor, format: EditorFormat): string {\n if (format === \"markdown\") return trimMarkdown(editor.getMarkdown());\n return editor.isEmpty ? \"\" : editor.getHTML();\n}\n\nconst toBoolean = (value: boolean | \"true\" | \"false\" | undefined) => value === true || value === \"true\";\n\nexport function EditorCore<THandle>({\n format,\n toolbar,\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 slot,\n handleRef,\n createHandle\n}: EditorCoreProps<THandle>) {\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 format\n });\n\n const [linkRequest, setLinkRequest] = React.useState(0);\n const requestLink = React.useCallback(() => setLinkRequest((count) => count + 1), []);\n const keymapHandlers = useAsRef<EditorKeymapHandlers>({\n onLinkShortcut: () => {\n if (!editable) return false;\n requestLink();\n return true;\n },\n onSubmitShortcut: () => {\n if (!onSubmit) return false;\n onSubmit();\n return true;\n }\n });\n\n const initialValue = value ?? defaultValue ?? \"\";\n const lastValue = React.useRef(initialValue);\n const [hiddenValue, setHiddenValue] = React.useState(initialValue);\n const pendingUploads = useLazyRef<PendingUploads>(() => new Set()).current;\n const fileInputRef = React.useRef<HTMLInputElement>(null);\n const pickImage = React.useCallback(() => fileInputRef.current?.click(), []);\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 ...createSchemaExtensions({\n format,\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 format,\n pickImage: latest.current.onImageUpload ? () => fileInputRef.current?.click() : undefined\n }),\n render: slashStore.render\n }),\n createImageUploadExtension(imageHandlers, pendingUploads),\n createEditorKeymap(keymapHandlers, format),\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 const contentPadding = variant === \"outline\" ? \"min-h-16 px-3 py-2\" : \"min-h-6\";\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(MARKDOWN_EDITOR_CONTENT_CLASS, contentPadding, contentClassName)\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 contentPadding,\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: initialValue,\n contentType: format,\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 next = readValue(current, format);\n lastValue.current = next;\n latest.current.onChange?.(next);\n if (latest.current.name) setHiddenValue(next);\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 === lastValue.current) return;\n editor.commands.setContent(value, { contentType: format, emitUpdate: false });\n lastValue.current = value;\n setHiddenValue(value);\n }, [editor, value, format]);\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, đọc trình soạn MỚI NHẤT qua ref. Dựng lại theo `editor` thì ai\n * giữ đối tượng cũ — một closure, một biến ngoài React — thấy `editor: null` mãi, vì trình soạn chỉ\n * có SAU lượt render đầu (`immediatelyRender: false`). Đã gặp ở harness.\n */\n const editorRef = useAsRef(editor);\n const core = React.useMemo<EditorCoreHandle>(\n () => ({\n focus: (position = \"end\") => {\n editorRef.current?.commands.focus(position);\n },\n getValue: () => (editorRef.current ? readValue(editorRef.current, format) : lastValue.current),\n setValue: (next) => {\n editorRef.current?.commands.setContent(next, { contentType: format, emitUpdate: true });\n },\n clear: () => {\n editorRef.current?.commands.clearContent(true);\n },\n isEmpty: () => (editorRef.current ? editorRef.current.isEmpty : lastValue.current.trim() === \"\"),\n isUploading: () => pendingUploads.size > 0,\n get editor() {\n return editorRef.current;\n }\n }),\n [editorRef, pendingUploads, format]\n );\n React.useImperativeHandle(handleRef, () => createHandle(core), [createHandle, core]);\n\n return (\n <div\n data-slot={slot}\n data-variant={variant}\n data-toolbar={toolbar}\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 {toolbar === \"fixed\" && editable && (\n <FixedToolbar\n editor={editor}\n labels={labels}\n format={format}\n onLink={requestLink}\n onPickImage={onImageUpload ? pickImage : undefined}\n // Khung `outline` bo góc mà KHÔNG `overflow-hidden` (nó sẽ giết `sticky`) — nền thanh tự bo\n // theo góc của khung. `ghost` không có khung: chừa khoảng cho chữ khỏi dính vào viền dưới.\n className={variant === \"outline\" ? \"rounded-t-[inherit]\" : \"mb-3\"}\n />\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={cn(contentPadding, contentClassName)} />\n )}\n\n {editor && editable && (\n <BubbleToolbar\n editor={editor}\n labels={labels}\n format={format}\n formatting={toolbar === \"bubble\"}\n linkRequest={linkRequest}\n />\n )}\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":[]}
@@ -0,0 +1,3 @@
1
+ import '@tiptap/core';
2
+ export { g as EditorBaseProps, f as EditorFormat, E as EditorHandleBase, a as EditorToolbarMode, b as MarkdownEditorMentionItem, c as MarkdownEditorMentionSource, d as MarkdownEditorMentions, e as MarkdownEditorVariant } from '../editor-types-BMST7-VR.js';
3
+ import './markdown-editor-constants.js';
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=editor-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ import { Editor } from '@tiptap/core';
3
+ import { f as EditorFormat } from '../editor-types-BMST7-VR.js';
4
+ import { MarkdownEditorLabels } from './markdown-editor-constants.js';
5
+
6
+ interface FixedToolbarProps {
7
+ /** `null` trước khi trình soạn dựng xong — thanh vẫn vẽ, mọi nút khoá. */
8
+ editor: Editor | null;
9
+ labels: MarkdownEditorLabels;
10
+ format: EditorFormat;
11
+ /** Mở ô nhập liên kết của bong bóng — như ⌘/Ctrl+K. */
12
+ onLink: () => void;
13
+ /** Có = app đã truyền `onImageUpload`; nút chọn ảnh hiện ra. */
14
+ onPickImage?: () => void;
15
+ className?: string;
16
+ }
17
+ declare function FixedToolbar({ editor, labels, format, onLink, onPickImage, className }: FixedToolbarProps): React.JSX.Element | null;
18
+
19
+ export { FixedToolbar, type FixedToolbarProps };
@@ -0,0 +1,106 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEditorState } from "@tiptap/react";
4
+ import { ImagePlus, Link2 } from "lucide-react";
5
+ import { Separator } from "../components/ui/separator.js";
6
+ import { cn } from "../lib/cn.js";
7
+ import {
8
+ actionsFor,
9
+ BLOCK_ACTIONS,
10
+ HISTORY_ACTIONS,
11
+ INSERT_ACTIONS,
12
+ MARK_ACTIONS
13
+ } from "./toolbar-actions.js";
14
+ import { ToolbarButton } from "./toolbar-button.js";
15
+ const ALL_ACTIONS = [
16
+ ...HISTORY_ACTIONS,
17
+ ...MARK_ACTIONS,
18
+ ...BLOCK_ACTIONS,
19
+ ...INSERT_ACTIONS
20
+ ];
21
+ function readState(editor) {
22
+ const active = {};
23
+ const enabled = {};
24
+ for (const action of ALL_ACTIONS) {
25
+ active[action.id] = editor ? action.isActive?.(editor) ?? false : false;
26
+ enabled[action.id] = editor ? action.canRun?.(editor) ?? true : false;
27
+ }
28
+ if (!editor) return { active, enabled, link: false, inTable: false, editable: false };
29
+ return {
30
+ active,
31
+ enabled,
32
+ link: editor.isActive("link"),
33
+ inTable: editor.isActive("table"),
34
+ editable: editor.isEditable
35
+ };
36
+ }
37
+ function sameState(a, b) {
38
+ if (!a || !b) return a === b;
39
+ if (a.link !== b.link || a.inTable !== b.inTable || a.editable !== b.editable) return false;
40
+ return ALL_ACTIONS.every(({ id }) => a.active[id] === b.active[id] && a.enabled[id] === b.enabled[id]);
41
+ }
42
+ function FixedToolbar({ editor, labels, format, onLink, onPickImage, className }) {
43
+ const state = useEditorState({
44
+ editor,
45
+ selector: ({ editor: current }) => readState(current),
46
+ equalityFn: sameState
47
+ });
48
+ if (!state) return null;
49
+ const blocksLocked = format === "markdown" && state.inTable;
50
+ const button = (action, locked = false) => /* @__PURE__ */ jsx(
51
+ ToolbarButton,
52
+ {
53
+ icon: action.icon,
54
+ label: labels[action.label],
55
+ keys: action.keys,
56
+ pressed: action.isActive ? !locked && Boolean(state.active[action.id]) : void 0,
57
+ disabled: locked || !state.editable || state.enabled[action.id] === false,
58
+ onClick: () => {
59
+ if (editor) action.run(editor);
60
+ }
61
+ },
62
+ action.id
63
+ );
64
+ const divider = /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-0.5 h-5" });
65
+ return /* @__PURE__ */ jsxs(
66
+ "div",
67
+ {
68
+ role: "toolbar",
69
+ "aria-label": labels.toolbar,
70
+ className: cn("sticky top-0 z-10 flex flex-wrap items-center gap-0.5 border-b bg-card p-1", className),
71
+ children: [
72
+ actionsFor(HISTORY_ACTIONS, format).map((action) => button(action)),
73
+ divider,
74
+ actionsFor(MARK_ACTIONS, format).map((action) => button(action)),
75
+ /* @__PURE__ */ jsx(
76
+ ToolbarButton,
77
+ {
78
+ icon: Link2,
79
+ label: labels.link,
80
+ keys: "mod+k",
81
+ pressed: state.link,
82
+ disabled: !state.editable,
83
+ onClick: onLink
84
+ }
85
+ ),
86
+ divider,
87
+ actionsFor(BLOCK_ACTIONS, format).map((action) => button(action, blocksLocked)),
88
+ divider,
89
+ actionsFor(INSERT_ACTIONS, format).map((action) => button(action, blocksLocked)),
90
+ onPickImage && /* @__PURE__ */ jsx(
91
+ ToolbarButton,
92
+ {
93
+ icon: ImagePlus,
94
+ label: labels.image,
95
+ disabled: !state.editable || blocksLocked,
96
+ onClick: onPickImage
97
+ }
98
+ )
99
+ ]
100
+ }
101
+ );
102
+ }
103
+ export {
104
+ FixedToolbar
105
+ };
106
+ //# sourceMappingURL=fixed-toolbar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/editor/fixed-toolbar.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Thanh công cụ CỐ ĐỊNH (`toolbar=\"fixed\"`) — kiểu CMS: mọi thao tác luôn nằm trước mắt, cho người\n * soạn không thuộc phím tắt hay cú pháp gõ tắt (trang tin, bài viết, mô tả sản phẩm).\n *\n * Dính ở đầu khung soạn (`sticky top-0`) để tài liệu dài vẫn còn thanh khi cuộn. Nền đặc `bg-card`\n * vì nội dung cuộn chui BÊN DƯỚI nó — một nền trong suốt là chữ đè lên nút.\n *\n * Cùng một bảng hành động với thanh nổi (`toolbar-actions.ts`). Liên kết không nằm trọn ở đây: nút\n * Liên kết chỉ mở ô nhập địa chỉ của bong bóng (`onLink`), vì ô đó phải đứng cạnh vùng chọn.\n *\n * Vẽ CẢ KHI trình soạn chưa dựng xong (`editor === null`, lượt render ở máy chủ và lượt hydrate đầu\n * — `immediatelyRender: false`): mọi nút hiện ra ở trạng thái khoá. Không thì thanh cao 37px (hai,\n * ba dòng ở màn hẹp) mọc ra SAU khi trang đã vẽ, đẩy cả nội dung bên dưới xuống.\n */\n\nimport type { Editor } from \"@tiptap/core\";\nimport { useEditorState } from \"@tiptap/react\";\nimport { ImagePlus, Link2 } from \"lucide-react\";\nimport { Separator } from \"../components/ui/separator.js\";\nimport { cn } from \"../lib/cn.js\";\nimport type { EditorFormat } from \"./editor-types.js\";\nimport type { MarkdownEditorLabels } from \"./markdown-editor-constants.js\";\nimport {\n actionsFor,\n BLOCK_ACTIONS,\n HISTORY_ACTIONS,\n INSERT_ACTIONS,\n MARK_ACTIONS,\n type ToolbarAction\n} from \"./toolbar-actions.js\";\nimport { ToolbarButton } from \"./toolbar-button.js\";\n\ninterface FixedToolbarState {\n active: Record<string, boolean>;\n enabled: Record<string, boolean>;\n link: boolean;\n inTable: boolean;\n editable: boolean;\n}\n\nconst ALL_ACTIONS: readonly ToolbarAction[] = [\n ...HISTORY_ACTIONS,\n ...MARK_ACTIONS,\n ...BLOCK_ACTIONS,\n ...INSERT_ACTIONS\n];\n\nfunction readState(editor: Editor | null): FixedToolbarState {\n const active: Record<string, boolean> = {};\n const enabled: Record<string, boolean> = {};\n for (const action of ALL_ACTIONS) {\n active[action.id] = editor ? (action.isActive?.(editor) ?? false) : false;\n enabled[action.id] = editor ? (action.canRun?.(editor) ?? true) : false;\n }\n if (!editor) return { active, enabled, link: false, inTable: false, editable: false };\n return {\n active,\n enabled,\n link: editor.isActive(\"link\"),\n inTable: editor.isActive(\"table\"),\n editable: editor.isEditable\n };\n}\n\nfunction sameState(a: FixedToolbarState | null, b: FixedToolbarState | null): boolean {\n if (!a || !b) return a === b;\n if (a.link !== b.link || a.inTable !== b.inTable || a.editable !== b.editable) return false;\n return ALL_ACTIONS.every(({ id }) => a.active[id] === b.active[id] && a.enabled[id] === b.enabled[id]);\n}\n\nexport interface FixedToolbarProps {\n /** `null` trước khi trình soạn dựng xong — thanh vẫn vẽ, mọi nút khoá. */\n editor: Editor | null;\n labels: MarkdownEditorLabels;\n format: EditorFormat;\n /** Mở ô nhập liên kết của bong bóng — như ⌘/Ctrl+K. */\n onLink: () => void;\n /** Có = app đã truyền `onImageUpload`; nút chọn ảnh hiện ra. */\n onPickImage?: () => void;\n className?: string;\n}\n\nexport function FixedToolbar({ editor, labels, format, onLink, onPickImage, className }: FixedToolbarProps) {\n const state = useEditorState({\n editor,\n selector: ({ editor: current }) => readState(current),\n equalityFn: sameState\n });\n if (!state) return null;\n\n /*\n * Ô bảng của trình soạn MARKDOWN chỉ chứa một đoạn văn (`ComitorTableCell`). Ở đó nút ĐỔI khối có bấm\n * cũng không làm gì; nút CHÈN khối thì tệ hơn — phép chèn của ProseMirror tách đôi cả bảng để khối vừa\n * (đã đo với \"Đường kẻ ngang\", xem `MarkdownTableCellGuard`). Khoá cả hai nhóm cho thấy điều đó.\n */\n const blocksLocked = format === \"markdown\" && state.inTable;\n const button = (action: ToolbarAction, locked = false) => (\n <ToolbarButton\n key={action.id}\n icon={action.icon}\n label={labels[action.label]}\n keys={action.keys}\n pressed={action.isActive ? !locked && Boolean(state.active[action.id]) : undefined}\n disabled={locked || !state.editable || state.enabled[action.id] === false}\n onClick={() => {\n if (editor) action.run(editor);\n }}\n />\n );\n const divider = <Separator orientation=\"vertical\" className=\"mx-0.5 h-5\" />;\n\n return (\n <div\n role=\"toolbar\"\n aria-label={labels.toolbar}\n className={cn(\"sticky top-0 z-10 flex flex-wrap items-center gap-0.5 border-b bg-card p-1\", className)}\n >\n {actionsFor(HISTORY_ACTIONS, format).map((action) => button(action))}\n {divider}\n {actionsFor(MARK_ACTIONS, format).map((action) => button(action))}\n <ToolbarButton\n icon={Link2}\n label={labels.link}\n keys=\"mod+k\"\n pressed={state.link}\n disabled={!state.editable}\n onClick={onLink}\n />\n {divider}\n {actionsFor(BLOCK_ACTIONS, format).map((action) => button(action, blocksLocked))}\n {divider}\n {actionsFor(INSERT_ACTIONS, format).map((action) => button(action, blocksLocked))}\n {onPickImage && (\n <ToolbarButton\n icon={ImagePlus}\n label={labels.image}\n disabled={!state.editable || blocksLocked}\n onClick={onPickImage}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,57 +1,3 @@
1
- import { Extension, Editor } from '@tiptap/core';
2
-
3
- /**
4
- * Ảnh dán / kéo thả / chọn từ menu `/` → tải qua `onImageUpload` của app → chèn vào tài liệu.
5
- *
6
- * Gói KHÔNG biết kho ảnh ở đâu: bucket của module Comitor là riêng tư, URL hiện được là URL đã ký
7
- * — nên đường tải là của app (route của app ký và ghi), gói chỉ trao `File` và nhận lại `src` để
8
- * LƯU. Cùng nguyên tắc với `/uploader` (`XHRUpload` về endpoint của app).
9
- *
10
- * ── ẢNH ĐANG TẢI LÀ DECORATION, KHÔNG PHẢI NODE ────────────────────────────────────────────────
11
- * Trong lúc tải, ảnh hiện NGAY bằng `blob:` của chính tệp — nhưng dưới dạng một widget decoration
12
- * (khuôn "upload" của ProseMirror), không phải một node trong tài liệu. Node ảnh thật chỉ được chèn
13
- * MỘT lần, khi app đã trả `src`.
14
- *
15
- * Bản đầu chèn một node ảnh TẠM rồi đổi `src` khi xong (giao dịch đổi không vào lịch sử). Nó qua
16
- * mọi phép thử lúc gõ, và hỏng ở lịch sử hoàn tác: ⌘Z gỡ ảnh, rồi ⌘⇧Z làm lại ĐÚNG bước chèn node
17
- * tạm — `src` là `blob:` đã thu hồi, cờ "đang tải" còn nguyên: một ảnh vỡ kẹt mãi, và vì cờ đó
18
- * nên nó cũng không bao giờ vào markdown. Với decoration thì tài liệu không bao giờ chứa thứ gì tạm:
19
- * lịch sử chỉ có một bước, "chèn ảnh thật".
20
- */
21
-
22
- /** Thứ `onImageUpload` trả về: `src` để LƯU, hoặc kèm `alt`/`title`. */
23
- type MarkdownEditorImageUploadResult = string | {
24
- src: string;
25
- alt?: string;
26
- title?: string;
27
- };
28
- interface ImageUploadHandlers {
29
- upload?: (file: File) => Promise<MarkdownEditorImageUploadResult>;
30
- onError: (error: unknown, file: File) => void;
31
- /** Đổi `src` lưu → `src` hiện (`resolveImageSrc`), để tải trước ảnh thật trước khi chèn. */
32
- resolveSrc?: (src: string) => string;
33
- /** Nhãn trợ năng của ảnh đang tải (`role="img"` của widget). */
34
- uploadingLabel: string;
35
- }
36
- /** Ảnh đang tải của MỘT trình soạn — `MarkdownEditorHandle.isUploading()` đọc nó. */
37
- type PendingUploads = Set<string>;
38
- /** Tệp ảnh trong một lần dán/thả. Tệp khác (PDF, zip…) bỏ qua: trình soạn chỉ nhúng ảnh. */
39
- declare function imageFilesFrom(data: DataTransfer | null): File[];
40
- /**
41
- * Hiện widget cho từng tệp và tải chúng. Xong tệp nào thì gỡ widget và chèn node ảnh thật vào ĐÚNG
42
- * chỗ widget đang đứng (vị trí đã đi theo mọi chỉnh sửa trong lúc chờ), trong CÙNG một giao dịch —
43
- * một bước hoàn tác. Không dời con trỏ: người dùng có thể đang gõ ở chỗ khác.
44
- */
45
- declare function insertImageFiles(editor: Editor, files: readonly File[], position: number | undefined, handlers: ImageUploadHandlers, pending: PendingUploads): void;
46
- /**
47
- * Bắt ảnh khi DÁN và khi THẢ, và giữ các widget đang tải. Không có `onImageUpload` thì plugin đứng
48
- * ngoài (trả `false`) — dán/thả đi đường mặc định của Tiptap, tức ảnh dạng tệp không được nhận
49
- * (không có chỗ nào để lưu nó).
50
- *
51
- * Kéo thả NỘI BỘ (di chuyển một ảnh đã có trong tài liệu, `moved === true`) thì không đụng tới.
52
- */
53
- declare function createImageUploadExtension(handlers: {
54
- readonly current: ImageUploadHandlers;
55
- }, pending: PendingUploads): Extension;
56
-
57
- export { type ImageUploadHandlers, type MarkdownEditorImageUploadResult, type PendingUploads, createImageUploadExtension, imageFilesFrom, insertImageFiles };
1
+ import '@tiptap/core';
2
+ export { I as ImageUploadHandlers, M as MarkdownEditorImageUploadResult, P as PendingUploads, h as createImageUploadExtension, i as imageFilesFrom, j as insertImageFiles } from '../editor-types-BMST7-VR.js';
3
+ import './markdown-editor-constants.js';
@@ -8,8 +8,9 @@ function imageFilesFrom(data) {
8
8
  function createUploadId() {
9
9
  return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
10
10
  }
11
- function blockInsertPosition(state, position) {
11
+ function blockInsertPosition(state, position, format) {
12
12
  const $pos = state.doc.resolve(Math.max(0, Math.min(position, state.doc.content.size)));
13
+ if (format === "html") return $pos.pos;
13
14
  for (let depth = $pos.depth; depth > 0; depth--) {
14
15
  if ($pos.node(depth).type.name === "table") return $pos.after(depth);
15
16
  }
@@ -69,7 +70,7 @@ function insertImageFiles(editor, files, position, handlers, pending) {
69
70
  tr.setMeta(UPLOAD_PLUGIN_KEY, removal);
70
71
  return true;
71
72
  }).insertContentAt(
72
- blockInsertPosition(editor.state, pos),
73
+ blockInsertPosition(editor.state, pos, handlers.format ?? "markdown"),
73
74
  { type: "image", attrs: { src: attrs.src, alt: attrs.alt ?? file.name, title: attrs.title ?? null } },
74
75
  { updateSelection: false }
75
76
  ).run();