@comitor/ui 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -5
- package/dist/components/ui/item.d.ts +1 -1
- package/dist/data-grid/use-data-grid.d.ts +2 -2
- package/dist/editor/bubble-toolbar.d.ts +34 -0
- package/dist/editor/bubble-toolbar.js +391 -0
- package/dist/editor/bubble-toolbar.js.map +1 -0
- package/dist/editor/image-upload.d.ts +57 -0
- package/dist/editor/image-upload.js +136 -0
- package/dist/editor/image-upload.js.map +1 -0
- package/dist/editor/index.d.ts +5 -0
- package/dist/editor/index.js +11 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/editor/markdown-editor-classes.d.ts +16 -0
- package/dist/editor/markdown-editor-classes.js +117 -0
- package/dist/editor/markdown-editor-classes.js.map +1 -0
- package/dist/editor/markdown-editor-constants.d.ts +92 -0
- package/dist/editor/markdown-editor-constants.js +6 -0
- package/dist/editor/markdown-editor-constants.js.map +1 -0
- package/dist/editor/markdown-editor.d.ts +125 -0
- package/dist/editor/markdown-editor.js +325 -0
- package/dist/editor/markdown-editor.js.map +1 -0
- package/dist/editor/markdown-extensions.d.ts +71 -0
- package/dist/editor/markdown-extensions.js +172 -0
- package/dist/editor/markdown-extensions.js.map +1 -0
- package/dist/editor/markdown-syntax.d.ts +87 -0
- package/dist/editor/markdown-syntax.js +58 -0
- package/dist/editor/markdown-syntax.js.map +1 -0
- package/dist/editor/slash-commands.d.ts +62 -0
- package/dist/editor/slash-commands.js +186 -0
- package/dist/editor/slash-commands.js.map +1 -0
- package/dist/editor/suggestion-menu.d.ts +38 -0
- package/dist/editor/suggestion-menu.js +144 -0
- package/dist/editor/suggestion-menu.js.map +1 -0
- package/dist/editor/suggestion-store.d.ts +47 -0
- package/dist/editor/suggestion-store.js +49 -0
- package/dist/editor/suggestion-store.js.map +1 -0
- package/dist/i18n/locale-provider.d.ts +1 -0
- package/dist/i18n/messages.d.ts +2 -0
- package/dist/i18n/messages.js +3 -0
- package/dist/i18n/messages.js.map +1 -1
- package/dist/i18n/namespaces/markdown-editor.d.ts +6 -0
- package/dist/i18n/namespaces/markdown-editor.js +127 -0
- package/dist/i18n/namespaces/markdown-editor.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/package.json +79 -2
|
@@ -0,0 +1,57 @@
|
|
|
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 };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
3
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
4
|
+
const UPLOAD_PLUGIN_KEY = new PluginKey("comitorImageUpload");
|
|
5
|
+
function imageFilesFrom(data) {
|
|
6
|
+
return Array.from(data?.files ?? []).filter((file) => file.type.startsWith("image/"));
|
|
7
|
+
}
|
|
8
|
+
function createUploadId() {
|
|
9
|
+
return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
10
|
+
}
|
|
11
|
+
function blockInsertPosition(state, position) {
|
|
12
|
+
const $pos = state.doc.resolve(Math.max(0, Math.min(position, state.doc.content.size)));
|
|
13
|
+
for (let depth = $pos.depth; depth > 0; depth--) {
|
|
14
|
+
if ($pos.node(depth).type.name === "table") return $pos.after(depth);
|
|
15
|
+
}
|
|
16
|
+
return $pos.pos;
|
|
17
|
+
}
|
|
18
|
+
function findPlaceholder(state, id) {
|
|
19
|
+
const found = UPLOAD_PLUGIN_KEY.getState(state)?.find(void 0, void 0, (spec) => spec.id === id);
|
|
20
|
+
return found?.[0]?.from ?? null;
|
|
21
|
+
}
|
|
22
|
+
function createPreview(src, label) {
|
|
23
|
+
const wrapper = document.createElement("span");
|
|
24
|
+
wrapper.setAttribute("data-upload-preview", "");
|
|
25
|
+
wrapper.setAttribute("role", "img");
|
|
26
|
+
wrapper.setAttribute("aria-label", label);
|
|
27
|
+
const image = document.createElement("img");
|
|
28
|
+
image.src = src;
|
|
29
|
+
image.alt = "";
|
|
30
|
+
wrapper.append(image);
|
|
31
|
+
return wrapper;
|
|
32
|
+
}
|
|
33
|
+
function preload(src) {
|
|
34
|
+
if (typeof window === "undefined" || typeof window.Image !== "function") return Promise.resolve();
|
|
35
|
+
const image = new window.Image();
|
|
36
|
+
image.src = src;
|
|
37
|
+
return Promise.race([image.decode().catch(() => void 0), new Promise((done) => setTimeout(done, 5e3))]);
|
|
38
|
+
}
|
|
39
|
+
function insertImageFiles(editor, files, position, handlers, pending) {
|
|
40
|
+
const upload = handlers.upload;
|
|
41
|
+
if (!upload || !editor.isEditable) return;
|
|
42
|
+
const at = position ?? editor.state.selection.from;
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
const id = createUploadId();
|
|
45
|
+
const previewSrc = URL.createObjectURL(file);
|
|
46
|
+
pending.add(id);
|
|
47
|
+
const meta = { add: { id, pos: at, src: previewSrc } };
|
|
48
|
+
editor.view.dispatch(editor.state.tr.setMeta(UPLOAD_PLUGIN_KEY, meta));
|
|
49
|
+
const settle = () => {
|
|
50
|
+
pending.delete(id);
|
|
51
|
+
URL.revokeObjectURL(previewSrc);
|
|
52
|
+
};
|
|
53
|
+
const removePreview = () => {
|
|
54
|
+
if (editor.isDestroyed) return;
|
|
55
|
+
const removal = { remove: { id } };
|
|
56
|
+
editor.view.dispatch(editor.state.tr.setMeta(UPLOAD_PLUGIN_KEY, removal));
|
|
57
|
+
};
|
|
58
|
+
upload(file).then(async (result) => {
|
|
59
|
+
const attrs = typeof result === "string" ? { src: result } : result;
|
|
60
|
+
await preload(handlers.resolveSrc?.(attrs.src) ?? attrs.src);
|
|
61
|
+
if (editor.isDestroyed) return settle();
|
|
62
|
+
const pos = findPlaceholder(editor.state, id);
|
|
63
|
+
if (pos === null) {
|
|
64
|
+
removePreview();
|
|
65
|
+
return settle();
|
|
66
|
+
}
|
|
67
|
+
const removal = { remove: { id } };
|
|
68
|
+
editor.chain().command(({ tr }) => {
|
|
69
|
+
tr.setMeta(UPLOAD_PLUGIN_KEY, removal);
|
|
70
|
+
return true;
|
|
71
|
+
}).insertContentAt(
|
|
72
|
+
blockInsertPosition(editor.state, pos),
|
|
73
|
+
{ type: "image", attrs: { src: attrs.src, alt: attrs.alt ?? file.name, title: attrs.title ?? null } },
|
|
74
|
+
{ updateSelection: false }
|
|
75
|
+
).run();
|
|
76
|
+
settle();
|
|
77
|
+
}).catch((error) => {
|
|
78
|
+
removePreview();
|
|
79
|
+
settle();
|
|
80
|
+
handlers.onError(error, file);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function createImageUploadExtension(handlers, pending) {
|
|
85
|
+
return Extension.create({
|
|
86
|
+
name: "comitorImageUpload",
|
|
87
|
+
addProseMirrorPlugins() {
|
|
88
|
+
const editor = this.editor;
|
|
89
|
+
return [
|
|
90
|
+
new Plugin({
|
|
91
|
+
key: UPLOAD_PLUGIN_KEY,
|
|
92
|
+
state: {
|
|
93
|
+
init: () => DecorationSet.empty,
|
|
94
|
+
apply(tr, set) {
|
|
95
|
+
let next = set.map(tr.mapping, tr.doc);
|
|
96
|
+
const meta = tr.getMeta(UPLOAD_PLUGIN_KEY);
|
|
97
|
+
if (meta && "add" in meta) {
|
|
98
|
+
const { id, pos, src } = meta.add;
|
|
99
|
+
const label = handlers.current.uploadingLabel;
|
|
100
|
+
const widget = Decoration.widget(pos, () => createPreview(src, label), { id, key: id, side: -1 });
|
|
101
|
+
next = next.add(tr.doc, [widget]);
|
|
102
|
+
} else if (meta && "remove" in meta) {
|
|
103
|
+
next = next.remove(next.find(void 0, void 0, (spec) => spec.id === meta.remove.id));
|
|
104
|
+
}
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
props: {
|
|
109
|
+
decorations: (state) => UPLOAD_PLUGIN_KEY.getState(state),
|
|
110
|
+
handlePaste: (view, event) => {
|
|
111
|
+
if (!handlers.current.upload || !view.editable) return false;
|
|
112
|
+
const files = imageFilesFrom(event.clipboardData);
|
|
113
|
+
if (files.length === 0) return false;
|
|
114
|
+
insertImageFiles(editor, files, void 0, handlers.current, pending);
|
|
115
|
+
return true;
|
|
116
|
+
},
|
|
117
|
+
handleDrop: (view, event, _slice, moved) => {
|
|
118
|
+
if (moved || !handlers.current.upload || !view.editable) return false;
|
|
119
|
+
const files = imageFilesFrom(event.dataTransfer);
|
|
120
|
+
if (files.length === 0) return false;
|
|
121
|
+
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
|
122
|
+
insertImageFiles(editor, files, coordinates?.pos, handlers.current, pending);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export {
|
|
132
|
+
createImageUploadExtension,
|
|
133
|
+
imageFilesFrom,
|
|
134
|
+
insertImageFiles
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=image-upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/editor/image-upload.ts"],"sourcesContent":["/**\n * Ả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.\n *\n * 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ý\n * — 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` để\n * LƯU. Cùng nguyên tắc với `/uploader` (`XHRUpload` về endpoint của app).\n *\n * ── ẢNH ĐANG TẢI LÀ DECORATION, KHÔNG PHẢI NODE ────────────────────────────────────────────────\n * 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\n * (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\n * MỘT lần, khi app đã trả `src`.\n *\n * 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\n * 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\n * 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ờ đó\n * 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:\n * lịch sử chỉ có một bước, \"chèn ảnh thật\".\n */\n\nimport { type Editor, Extension } from \"@tiptap/core\";\nimport { type EditorState, Plugin, PluginKey } from \"@tiptap/pm/state\";\nimport { Decoration, DecorationSet } from \"@tiptap/pm/view\";\n\n/** Thứ `onImageUpload` trả về: `src` để LƯU, hoặc kèm `alt`/`title`. */\nexport type MarkdownEditorImageUploadResult = string | { src: string; alt?: string; title?: string };\n\nexport interface ImageUploadHandlers {\n upload?: (file: File) => Promise<MarkdownEditorImageUploadResult>;\n onError: (error: unknown, file: File) => void;\n /** Đổi `src` lưu → `src` hiện (`resolveImageSrc`), để tải trước ảnh thật trước khi chèn. */\n resolveSrc?: (src: string) => string;\n /** Nhãn trợ năng của ảnh đang tải (`role=\"img\"` của widget). */\n uploadingLabel: string;\n}\n\n/** Ảnh đang tải của MỘT trình soạn — `MarkdownEditorHandle.isUploading()` đọc nó. */\nexport type PendingUploads = Set<string>;\n\ntype UploadMeta = { add: { id: string; pos: number; src: string } } | { remove: { id: string } };\n\nconst UPLOAD_PLUGIN_KEY = new PluginKey<DecorationSet>(\"comitorImageUpload\");\n\n/** 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. */\nexport function imageFilesFrom(data: DataTransfer | null): File[] {\n return Array.from(data?.files ?? []).filter((file) => file.type.startsWith(\"image/\"));\n}\n\n/**\n * `crypto.randomUUID` chỉ có ở NGỮ CẢNH AN TOÀN — mở trang dev bằng IP mạng LAN qua http thì nó\n * `undefined`, và một `TypeError` giữa lúc dán ảnh là mất luôn tấm ảnh. Id này chỉ cần duy nhất\n * trong một tài liệu đang mở.\n */\nfunction createUploadId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Vị trí chèn một ảnh (node KHỐI) gần `position` nhất. Trong bảng thì ra SAU bảng: ô bảng chỉ chứa\n * một đoạn văn (`ComitorTableCell`), chèn khối vào đó là một giao dịch không hợp lệ.\n */\nfunction blockInsertPosition(state: EditorState, position: number): number {\n const $pos = state.doc.resolve(Math.max(0, Math.min(position, state.doc.content.size)));\n for (let depth = $pos.depth; depth > 0; depth--) {\n if ($pos.node(depth).type.name === \"table\") return $pos.after(depth);\n }\n return $pos.pos;\n}\n\n/** Vị trí HIỆN TẠI của widget đang tải — đã đi theo mọi giao dịch từ lúc dán. `null` = đã bị xoá cùng vùng chữ. */\nfunction findPlaceholder(state: EditorState, id: string): number | null {\n const found = UPLOAD_PLUGIN_KEY.getState(state)?.find(undefined, undefined, (spec) => spec.id === id);\n return found?.[0]?.from ?? null;\n}\n\n/**\n * Widget ảnh đang tải. Lớp kiểu nằm ở `markdown-editor-classes.ts` (`[data-upload-preview]`) — DOM\n * này không qua JSX nên không gắn `className` có nghĩa được ở đây ngoài một thuộc tính để nhắm.\n */\nfunction createPreview(src: string, label: string): HTMLElement {\n const wrapper = document.createElement(\"span\");\n wrapper.setAttribute(\"data-upload-preview\", \"\");\n wrapper.setAttribute(\"role\", \"img\");\n wrapper.setAttribute(\"aria-label\", label);\n const image = document.createElement(\"img\");\n image.src = src;\n image.alt = \"\";\n wrapper.append(image);\n return wrapper;\n}\n\n/**\n * Tải trước ảnh thật rồi mới chèn — không thì ảnh nháy TRẮNG giữa lúc widget đã gỡ còn ảnh mạng chưa\n * về. Lỗi tải trước hay quá 5 giây thì vẫn chèn: thà một ảnh vỡ thấy được còn hơn treo mãi ở trạng\n * thái \"đang tải\".\n */\nfunction preload(src: string): Promise<void> {\n if (typeof window === \"undefined\" || typeof window.Image !== \"function\") return Promise.resolve();\n const image = new window.Image();\n image.src = src;\n return Promise.race([image.decode().catch(() => undefined), new Promise<void>((done) => setTimeout(done, 5000))]);\n}\n\n/**\n * 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\n * 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 —\n * 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.\n */\nexport function insertImageFiles(\n editor: Editor,\n files: readonly File[],\n position: number | undefined,\n handlers: ImageUploadHandlers,\n pending: PendingUploads\n): void {\n const upload = handlers.upload;\n if (!upload || !editor.isEditable) return;\n\n const at = position ?? editor.state.selection.from;\n for (const file of files) {\n const id = createUploadId();\n const previewSrc = URL.createObjectURL(file);\n pending.add(id);\n const meta: UploadMeta = { add: { id, pos: at, src: previewSrc } };\n editor.view.dispatch(editor.state.tr.setMeta(UPLOAD_PLUGIN_KEY, meta));\n\n const settle = () => {\n pending.delete(id);\n URL.revokeObjectURL(previewSrc);\n };\n const removePreview = () => {\n if (editor.isDestroyed) return;\n const removal: UploadMeta = { remove: { id } };\n editor.view.dispatch(editor.state.tr.setMeta(UPLOAD_PLUGIN_KEY, removal));\n };\n\n upload(file)\n .then(async (result) => {\n const attrs = typeof result === \"string\" ? { src: result } : result;\n await preload(handlers.resolveSrc?.(attrs.src) ?? attrs.src);\n if (editor.isDestroyed) return settle();\n const pos = findPlaceholder(editor.state, id);\n if (pos === null) {\n // Vùng chữ chứa widget đã bị xoá trong lúc chờ: người dùng đã bỏ tấm ảnh này.\n removePreview();\n return settle();\n }\n const removal: UploadMeta = { remove: { id } };\n editor\n .chain()\n .command(({ tr }) => {\n tr.setMeta(UPLOAD_PLUGIN_KEY, removal);\n return true;\n })\n .insertContentAt(\n blockInsertPosition(editor.state, pos),\n { type: \"image\", attrs: { src: attrs.src, alt: attrs.alt ?? file.name, title: attrs.title ?? null } },\n { updateSelection: false }\n )\n .run();\n settle();\n })\n .catch((error: unknown) => {\n removePreview();\n settle();\n handlers.onError(error, file);\n });\n }\n}\n\n/**\n * Bắt ảnh khi DÁN và khi THẢ, và giữ các widget đang tải. Không có `onImageUpload` thì plugin đứng\n * 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\n * (không có chỗ nào để lưu nó).\n *\n * 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.\n */\nexport function createImageUploadExtension(\n handlers: { readonly current: ImageUploadHandlers },\n pending: PendingUploads\n): Extension {\n return Extension.create({\n name: \"comitorImageUpload\",\n addProseMirrorPlugins() {\n const editor = this.editor;\n return [\n new Plugin<DecorationSet>({\n key: UPLOAD_PLUGIN_KEY,\n state: {\n init: () => DecorationSet.empty,\n apply(tr, set) {\n let next = set.map(tr.mapping, tr.doc);\n const meta = tr.getMeta(UPLOAD_PLUGIN_KEY) as UploadMeta | undefined;\n if (meta && \"add\" in meta) {\n const { id, pos, src } = meta.add;\n const label = handlers.current.uploadingLabel;\n // `side: -1`: chữ gõ tiếp ngay tại chỗ dán đi SAU widget — như thể con trỏ đã đứng\n // sau ảnh. Mặc định (0) đẩy widget sang phải, và câu gõ tiếp nằm trước ảnh.\n const widget = Decoration.widget(pos, () => createPreview(src, label), { id, key: id, side: -1 });\n next = next.add(tr.doc, [widget]);\n } else if (meta && \"remove\" in meta) {\n next = next.remove(next.find(undefined, undefined, (spec) => spec.id === meta.remove.id));\n }\n return next;\n }\n },\n props: {\n decorations: (state) => UPLOAD_PLUGIN_KEY.getState(state),\n handlePaste: (view, event) => {\n if (!handlers.current.upload || !view.editable) return false;\n const files = imageFilesFrom(event.clipboardData);\n if (files.length === 0) return false;\n insertImageFiles(editor, files, undefined, handlers.current, pending);\n return true;\n },\n handleDrop: (view, event, _slice, moved) => {\n if (moved || !handlers.current.upload || !view.editable) return false;\n const files = imageFilesFrom(event.dataTransfer);\n if (files.length === 0) return false;\n const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });\n insertImageFiles(editor, files, coordinates?.pos, handlers.current, pending);\n return true;\n }\n }\n })\n ];\n }\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { MarkdownEditorImageUploadResult } from './image-upload.js';
|
|
2
|
+
export { MarkdownEditor, MarkdownEditorHandle, MarkdownEditorMentionItem, MarkdownEditorMentionSource, MarkdownEditorMentions, MarkdownEditorProps, MarkdownEditorVariant } from './markdown-editor.js';
|
|
3
|
+
export { DEFAULT_MARKDOWN_EDITOR_LABELS, MarkdownEditorBlockId, MarkdownEditorLabels } from './markdown-editor-constants.js';
|
|
4
|
+
import '@tiptap/core';
|
|
5
|
+
import 'react';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/editor/index.ts"],"sourcesContent":["/**\n * Entry `@comitor/ui/editor` — trình soạn văn bản giàu định dạng kiểu Linear, lưu bằng markdown.\n *\n * ── VÌ SAO LÀ MỘT ENTRY RIÊNG ────────────────────────────────────────────────────────────────\n * Cùng lý do `/data-grid` và `/uploader`: trình soạn kéo Tiptap/ProseMirror (~184KB gzip, cộng ~53KB\n * bảng tô màu mã) qua các peer TUỲ CHỌN, mà `tsup` chạy `bundle: false` — để nó ở entry `.` là bắt\n * mọi app cài Tiptap chỉ để dùng `Button`. App không `import` từ đây thì không có một byte nào.\n *\n * ── VÌ SAO TIPTAP LÀ PEER, KHÔNG PHẢI DEPENDENCY ────────────────────────────────────────────\n * Mọi gói `@tiptap/*` ghim `@tiptap/core` + `@tiptap/pm` bằng phiên bản CHÍNH XÁC. App sẽ tự viết\n * extension (vd node `KEY-n` của module Tasks) bằng `@tiptap/core` của nó — nếu gói giữ một bản\n * riêng thì trang có hai bản ProseMirror, và node tạo bằng bản này không lắp được vào tài liệu của\n * bản kia. Cùng loại \"context singleton\" với `next-themes` và `@tanstack/react-table`.\n *\n * Cài ở app — MỌI gói `@tiptap/*` CÙNG một phiên bản:\n * ```bash\n * pnpm add @tiptap/core @tiptap/pm @tiptap/react @tiptap/starter-kit @tiptap/markdown \\\n * @tiptap/extensions @tiptap/extension-list @tiptap/extension-table @tiptap/extension-image \\\n * @tiptap/extension-mention @tiptap/suggestion @tiptap/extension-paragraph \\\n * @tiptap/extension-code-block-lowlight lowlight highlight.js\n * ```\n *\n * Tài liệu: docs/editor.md của repo `comitor-ui`.\n */\n\nexport type { MarkdownEditorImageUploadResult } from \"./image-upload.js\";\nexport {\n MarkdownEditor,\n type MarkdownEditorHandle,\n type MarkdownEditorMentionItem,\n type MarkdownEditorMentionSource,\n type MarkdownEditorMentions,\n type MarkdownEditorProps,\n type MarkdownEditorVariant\n} from \"./markdown-editor.js\";\nexport {\n DEFAULT_MARKDOWN_EDITOR_LABELS,\n type MarkdownEditorBlockId,\n type MarkdownEditorLabels\n} from \"./markdown-editor-constants.js\";\n"],"mappings":"AA0BA;AAAA,EACE;AAAA,OAOK;AACP;AAAA,EACE;AAAA,OAGK;","names":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kiểu chữ của vùng soạn — file thuần, không `"use client"`.
|
|
3
|
+
*
|
|
4
|
+
* DOM bên trong do ProseMirror dựng (không phải JSX của gói), nên không có `className` nào gắn được
|
|
5
|
+
* lên từng `<h2>`, `<li>`, `<pre>`: mọi kiểu đi qua biến thể con cháu `[&_h2]:…` đặt trên PHẦN TỬ
|
|
6
|
+
* GỐC của ProseMirror. Tailwind của app vẫn sinh được chúng vì chuỗi nằm nguyên văn trong `dist/`
|
|
7
|
+
* (`@source "./dist"` của `styles.css`) — nên KHÔNG dựng chuỗi lớp động ở đây.
|
|
8
|
+
*
|
|
9
|
+
* Màu chỉ đi qua token VAI TRÒ, đúng hợp đồng của gói: chữ thường kế thừa `--foreground`, liên kết
|
|
10
|
+
* `--link-ink`, mã `--muted`, nhắc tên `--info/15` (cùng khối của `Mention`), tô màu cú pháp các bậc
|
|
11
|
+
* `-ink` — chúng là bậc dành cho CHỮ trên nền trang/muted ở cả hai bảng màu.
|
|
12
|
+
*/
|
|
13
|
+
/** Toàn bộ lớp của phần tử gốc ProseMirror (`.tiptap`). */
|
|
14
|
+
declare const MARKDOWN_EDITOR_CONTENT_CLASS: string;
|
|
15
|
+
|
|
16
|
+
export { MARKDOWN_EDITOR_CONTENT_CLASS };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const LAYOUT = [
|
|
2
|
+
"min-w-0 text-base leading-relaxed text-foreground outline-none md:text-sm",
|
|
3
|
+
"[&>*+*]:mt-3 [&>*:first-child]:mt-0",
|
|
4
|
+
"[&>h1]:mt-6 [&>h2]:mt-5 [&>h3]:mt-4"
|
|
5
|
+
];
|
|
6
|
+
const HEADINGS = [
|
|
7
|
+
"[&_h1]:text-xl [&_h1]:leading-snug [&_h1]:font-semibold [&_h1]:tracking-tight",
|
|
8
|
+
"[&_h2]:text-lg [&_h2]:leading-snug [&_h2]:font-semibold",
|
|
9
|
+
"[&_h3]:text-base [&_h3]:leading-snug [&_h3]:font-semibold",
|
|
10
|
+
"[&_h4]:font-semibold [&_h5]:font-semibold [&_h6]:font-semibold"
|
|
11
|
+
];
|
|
12
|
+
const INLINE = [
|
|
13
|
+
"[&_strong]:font-semibold",
|
|
14
|
+
// Liên kết: khuôn của `Button variant="link"` / `FieldDescription` — bậc `--link-ink`, gạch dưới.
|
|
15
|
+
"[&_a]:text-link-ink [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:text-link-ink-hover",
|
|
16
|
+
"[&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5",
|
|
17
|
+
"[&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[0.875em]",
|
|
18
|
+
// Nhắc tên: cùng cặp với khối `@Tên` của `Mention` (nền `info/15`, chữ thường) — đã đo ở mention.tsx.
|
|
19
|
+
"[&_[data-type=mention]]:rounded-sm [&_[data-type=mention]]:bg-info/15 [&_[data-type=mention]]:px-0.5",
|
|
20
|
+
"[&_[data-type=mention]]:py-px [&_[data-type=mention]]:font-medium [&_[data-type=mention]]:text-foreground"
|
|
21
|
+
];
|
|
22
|
+
const LISTS = [
|
|
23
|
+
"[&_ul:not([data-type=taskList])]:list-disc [&_ol]:list-decimal",
|
|
24
|
+
"[&_ul:not([data-type=taskList])]:pl-6 [&_ol]:pl-6 [&_li]:marker:text-muted-foreground",
|
|
25
|
+
"[&_li+li]:mt-1 [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_li>p]:my-0",
|
|
26
|
+
"[&_ul[data-type=taskList]]:list-none [&_ul[data-type=taskList]]:pl-0.5",
|
|
27
|
+
"[&_[data-type=taskList]>li]:flex [&_[data-type=taskList]>li]:items-start [&_[data-type=taskList]>li]:gap-2",
|
|
28
|
+
"[&_[data-type=taskList]>li>label]:flex [&_[data-type=taskList]>li>label]:h-[1.625em] [&_[data-type=taskList]>li>label]:items-center",
|
|
29
|
+
"[&_[data-type=taskList]>li>label]:select-none [&_[data-type=taskList]>li>div]:min-w-0 [&_[data-type=taskList]>li>div]:flex-1",
|
|
30
|
+
// Soạn được: con trỏ tay trên ô tick. Chỉ đọc: ô không nhận chuột — Tiptap để nó bấm được rồi tự lật
|
|
31
|
+
// lại (không có `onReadOnlyChecked`), nên một cú bấm "không ăn" là thứ duy nhất người dùng thấy.
|
|
32
|
+
"[&[contenteditable=true]_[data-type=taskList]>li>label>input]:cursor-pointer",
|
|
33
|
+
"[&[contenteditable=false]_[data-type=taskList]>li>label]:pointer-events-none",
|
|
34
|
+
// Mục đã xong: chữ nhạt + gạch — CHỈ đoạn của chính mục, không lan xuống danh sách con chưa xong.
|
|
35
|
+
"[&_li[data-checked=true]>div>p]:text-muted-foreground [&_li[data-checked=true]>div>p]:line-through"
|
|
36
|
+
];
|
|
37
|
+
const TASK_CHECKBOX = [
|
|
38
|
+
"[&_[data-type=taskList]>li>label>input]:relative [&_[data-type=taskList]>li>label>input]:m-0 [&_[data-type=taskList]>li>label>input]:grid",
|
|
39
|
+
"[&_[data-type=taskList]>li>label>input]:size-4 [&_[data-type=taskList]>li>label>input]:shrink-0 [&_[data-type=taskList]>li>label>input]:appearance-none",
|
|
40
|
+
"[&_[data-type=taskList]>li>label>input]:place-content-center [&_[data-type=taskList]>li>label>input]:rounded-[4px]",
|
|
41
|
+
"[&_[data-type=taskList]>li>label>input]:border [&_[data-type=taskList]>li>label>input]:border-choice-edge",
|
|
42
|
+
"[&_[data-type=taskList]>li>label>input]:bg-transparent [&_[data-type=taskList]>li>label>input]:transition-colors",
|
|
43
|
+
"[&_[data-type=taskList]>li>label>input:checked]:border-control-on [&_[data-type=taskList]>li>label>input:checked]:bg-control-on",
|
|
44
|
+
"[&_[data-type=taskList]>li>label>input:focus-visible]:outline-2 [&_[data-type=taskList]>li>label>input:focus-visible]:outline-offset-2",
|
|
45
|
+
"[&_[data-type=taskList]>li>label>input:focus-visible]:outline-ring",
|
|
46
|
+
// Dấu tick CHỈ tồn tại khi đã tick (`content` chỉ có ở `:checked`) — không ẩn bằng `opacity-0`: một
|
|
47
|
+
// dấu vô hình vẫn là một nét mà cổng màu phải đo, và đo ra 1,00:1.
|
|
48
|
+
"[&_[data-type=taskList]>li>label>input:checked]:before:h-2 [&_[data-type=taskList]>li>label>input:checked]:before:w-1",
|
|
49
|
+
"[&_[data-type=taskList]>li>label>input:checked]:before:-translate-y-px [&_[data-type=taskList]>li>label>input:checked]:before:rotate-45",
|
|
50
|
+
"[&_[data-type=taskList]>li>label>input:checked]:before:border-r-2 [&_[data-type=taskList]>li>label>input:checked]:before:border-b-2",
|
|
51
|
+
"[&_[data-type=taskList]>li>label>input:checked]:before:border-control-check",
|
|
52
|
+
"[&_[data-type=taskList]>li>label>input:checked]:before:content-['']"
|
|
53
|
+
];
|
|
54
|
+
const BLOCKS = [
|
|
55
|
+
"[&_blockquote]:border-l-2 [&_blockquote]:border-muted-foreground [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground",
|
|
56
|
+
"[&_blockquote>*+*]:mt-2",
|
|
57
|
+
"[&_hr]:my-1 [&_hr]:border-border",
|
|
58
|
+
// Khối mã XUỐNG DÒNG (`pre-wrap`) thay vì cuộn ngang: trong trình soạn, một thanh cuộn ngang lồng
|
|
59
|
+
// trong khung cuộn dọc là chỗ con trỏ biến mất khỏi tầm nhìn khi gõ. Đó cũng là CSS Tiptap chèn sẵn
|
|
60
|
+
// (`.ProseMirror pre`) — bản trước ghi `whitespace-pre` ở đây, và nó thua luật đó mà không ai thấy.
|
|
61
|
+
"[&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:px-4 [&_pre]:py-3",
|
|
62
|
+
"[&_pre]:font-mono [&_pre]:text-[0.8125rem] [&_pre]:leading-relaxed [&_pre]:whitespace-pre-wrap",
|
|
63
|
+
"[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-inherit"
|
|
64
|
+
];
|
|
65
|
+
const SYNTAX = [
|
|
66
|
+
"[&_.hljs-comment]:text-muted-foreground [&_.hljs-comment]:italic [&_.hljs-quote]:text-muted-foreground",
|
|
67
|
+
"[&_.hljs-keyword]:text-red-ink [&_.hljs-selector-tag]:text-red-ink [&_.hljs-doctag]:text-red-ink",
|
|
68
|
+
"[&_.hljs-meta_.hljs-keyword]:text-red-ink [&_.hljs-template-tag]:text-red-ink",
|
|
69
|
+
"[&_.hljs-string]:text-navy-ink [&_.hljs-regexp]:text-navy-ink [&_.hljs-meta_.hljs-string]:text-navy-ink",
|
|
70
|
+
"[&_.hljs-number]:text-teal-ink [&_.hljs-literal]:text-teal-ink [&_.hljs-attr]:text-teal-ink",
|
|
71
|
+
"[&_.hljs-attribute]:text-teal-ink [&_.hljs-variable]:text-teal-ink [&_.hljs-template-variable]:text-teal-ink",
|
|
72
|
+
"[&_.hljs-selector-attr]:text-teal-ink [&_.hljs-selector-class]:text-teal-ink [&_.hljs-selector-id]:text-teal-ink",
|
|
73
|
+
"[&_.hljs-title]:text-orange-ink [&_.hljs-section]:text-orange-ink [&_.hljs-name]:text-orange-ink",
|
|
74
|
+
"[&_.hljs-type]:text-orange-ink [&_.hljs-built_in]:text-orange-ink [&_.hljs-symbol]:text-orange-ink",
|
|
75
|
+
"[&_.hljs-meta]:text-muted-foreground [&_.hljs-bullet]:text-orange-ink [&_.hljs-link]:underline",
|
|
76
|
+
"[&_.hljs-addition]:text-success-ink [&_.hljs-deletion]:text-destructive-ink",
|
|
77
|
+
"[&_.hljs-emphasis]:italic [&_.hljs-strong]:font-semibold"
|
|
78
|
+
];
|
|
79
|
+
const TABLES = [
|
|
80
|
+
"[&_.tableWrapper]:overflow-x-auto",
|
|
81
|
+
"[&_table]:w-full [&_table]:border-collapse [&_table]:text-left",
|
|
82
|
+
"[&_td]:border [&_td]:border-border [&_td]:px-2.5 [&_td]:py-1.5 [&_td]:align-top",
|
|
83
|
+
"[&_th]:border [&_th]:border-border [&_th]:bg-muted [&_th]:px-2.5 [&_th]:py-1.5 [&_th]:align-top [&_th]:font-medium",
|
|
84
|
+
"[&_td>p]:my-0 [&_th>p]:my-0 [&_.selectedCell]:bg-accent [&_.selectedCell]:text-accent-foreground"
|
|
85
|
+
];
|
|
86
|
+
const MEDIA = [
|
|
87
|
+
"[&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-md [&_img]:border [&_img]:border-border",
|
|
88
|
+
// Ảnh đang tải lên — một widget decoration, không phải node (xem `image-upload.ts`): đứng thành
|
|
89
|
+
// một khối riêng, mờ và nhấp nháy tới khi ảnh thật được chèn vào chỗ nó.
|
|
90
|
+
"[&_[data-upload-preview]]:my-2 [&_[data-upload-preview]]:block [&_[data-upload-preview]]:w-fit",
|
|
91
|
+
"[&_[data-upload-preview]>img]:animate-pulse [&_[data-upload-preview]>img]:opacity-60",
|
|
92
|
+
// Node được chọn bằng bấm (ảnh, đường kẻ): viền focus của gói, không phải nền.
|
|
93
|
+
"[&_.ProseMirror-selectednode]:outline-2 [&_.ProseMirror-selectednode]:outline-offset-2 [&_.ProseMirror-selectednode]:outline-ring"
|
|
94
|
+
];
|
|
95
|
+
const AFFORDANCES = [
|
|
96
|
+
"[&_.is-editor-empty:first-child]:before:pointer-events-none [&_.is-editor-empty:first-child]:before:float-left",
|
|
97
|
+
"[&_.is-editor-empty:first-child]:before:h-0 [&_.is-editor-empty:first-child]:before:text-muted-foreground",
|
|
98
|
+
"[&_.is-editor-empty:first-child]:before:content-[attr(data-placeholder)]",
|
|
99
|
+
"[&_.suggestion]:rounded-sm [&_.suggestion]:bg-muted",
|
|
100
|
+
"[&_.ProseMirror-gapcursor]:after:border-foreground"
|
|
101
|
+
];
|
|
102
|
+
const MARKDOWN_EDITOR_CONTENT_CLASS = [
|
|
103
|
+
...LAYOUT,
|
|
104
|
+
...HEADINGS,
|
|
105
|
+
...INLINE,
|
|
106
|
+
...LISTS,
|
|
107
|
+
...TASK_CHECKBOX,
|
|
108
|
+
...BLOCKS,
|
|
109
|
+
...SYNTAX,
|
|
110
|
+
...TABLES,
|
|
111
|
+
...MEDIA,
|
|
112
|
+
...AFFORDANCES
|
|
113
|
+
].join(" ");
|
|
114
|
+
export {
|
|
115
|
+
MARKDOWN_EDITOR_CONTENT_CLASS
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=markdown-editor-classes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/editor/markdown-editor-classes.ts"],"sourcesContent":["/**\n * Kiểu chữ của vùng soạn — file thuần, không `\"use client\"`.\n *\n * DOM bên trong do ProseMirror dựng (không phải JSX của gói), nên không có `className` nào gắn được\n * lên từng `<h2>`, `<li>`, `<pre>`: mọi kiểu đi qua biến thể con cháu `[&_h2]:…` đặt trên PHẦN TỬ\n * GỐC của ProseMirror. Tailwind của app vẫn sinh được chúng vì chuỗi nằm nguyên văn trong `dist/`\n * (`@source \"./dist\"` của `styles.css`) — nên KHÔNG dựng chuỗi lớp động ở đây.\n *\n * Màu chỉ đi qua token VAI TRÒ, đúng hợp đồng của gói: chữ thường kế thừa `--foreground`, liên kết\n * `--link-ink`, mã `--muted`, nhắc tên `--info/15` (cùng khối của `Mention`), tô màu cú pháp các bậc\n * `-ink` — chúng là bậc dành cho CHỮ trên nền trang/muted ở cả hai bảng màu.\n */\n\n/*\n * Bố cục khối: khoảng cách giữa các khối CẤP ĐẦU bằng `> * + *` (không margin hai đầu tài liệu),\n * tiêu đề cách xa hơn phía trên để nhóm với phần nó mở. Cỡ chữ theo `Textarea`: 16px ở màn hẹp —\n * dưới 16px là Safari iOS tự phóng to trang khi chạm vào vùng soạn — 14px từ `md`.\n */\nconst LAYOUT = [\n \"min-w-0 text-base leading-relaxed text-foreground outline-none md:text-sm\",\n \"[&>*+*]:mt-3 [&>*:first-child]:mt-0\",\n \"[&>h1]:mt-6 [&>h2]:mt-5 [&>h3]:mt-4\"\n];\n\nconst HEADINGS = [\n \"[&_h1]:text-xl [&_h1]:leading-snug [&_h1]:font-semibold [&_h1]:tracking-tight\",\n \"[&_h2]:text-lg [&_h2]:leading-snug [&_h2]:font-semibold\",\n \"[&_h3]:text-base [&_h3]:leading-snug [&_h3]:font-semibold\",\n \"[&_h4]:font-semibold [&_h5]:font-semibold [&_h6]:font-semibold\"\n];\n\nconst INLINE = [\n \"[&_strong]:font-semibold\",\n // Liên kết: khuôn của `Button variant=\"link\"` / `FieldDescription` — bậc `--link-ink`, gạch dưới.\n \"[&_a]:text-link-ink [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:text-link-ink-hover\",\n \"[&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5\",\n \"[&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[0.875em]\",\n // Nhắc tên: cùng cặp với khối `@Tên` của `Mention` (nền `info/15`, chữ thường) — đã đo ở mention.tsx.\n \"[&_[data-type=mention]]:rounded-sm [&_[data-type=mention]]:bg-info/15 [&_[data-type=mention]]:px-0.5\",\n \"[&_[data-type=mention]]:py-px [&_[data-type=mention]]:font-medium [&_[data-type=mention]]:text-foreground\"\n];\n\n/*\n * Danh sách thường. Danh sách việc (`ul[data-type=taskList]`) bỏ chấm đầu dòng và tự vẽ ô tick: DOM\n * do `TaskItem` dựng là `li[data-checked] > label > input + span(sr-only)` cạnh `div` nội dung.\n * ⚠ `<li>` KHÔNG mang `data-type=\"taskItem\"`: thuộc tính đó chỉ có trong `renderHTML`, còn node view\n * (thứ thật sự vẽ trong trình soạn) không gắn nó — đã đo trên 3.31.4, selector `li[data-type=…]`\n * trượt hết và ô tick nằm một dòng, chữ rơi xuống dòng dưới. Nhắm qua `ul` cha.\n */\nconst LISTS = [\n \"[&_ul:not([data-type=taskList])]:list-disc [&_ol]:list-decimal\",\n \"[&_ul:not([data-type=taskList])]:pl-6 [&_ol]:pl-6 [&_li]:marker:text-muted-foreground\",\n \"[&_li+li]:mt-1 [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_li>p]:my-0\",\n \"[&_ul[data-type=taskList]]:list-none [&_ul[data-type=taskList]]:pl-0.5\",\n \"[&_[data-type=taskList]>li]:flex [&_[data-type=taskList]>li]:items-start [&_[data-type=taskList]>li]:gap-2\",\n \"[&_[data-type=taskList]>li>label]:flex [&_[data-type=taskList]>li>label]:h-[1.625em] [&_[data-type=taskList]>li>label]:items-center\",\n \"[&_[data-type=taskList]>li>label]:select-none [&_[data-type=taskList]>li>div]:min-w-0 [&_[data-type=taskList]>li>div]:flex-1\",\n // Soạn được: con trỏ tay trên ô tick. Chỉ đọc: ô không nhận chuột — Tiptap để nó bấm được rồi tự lật\n // lại (không có `onReadOnlyChecked`), nên một cú bấm \"không ăn\" là thứ duy nhất người dùng thấy.\n \"[&[contenteditable=true]_[data-type=taskList]>li>label>input]:cursor-pointer\",\n \"[&[contenteditable=false]_[data-type=taskList]>li>label]:pointer-events-none\",\n // Mục đã xong: chữ nhạt + gạch — CHỈ đoạn của chính mục, không lan xuống danh sách con chưa xong.\n \"[&_li[data-checked=true]>div>p]:text-muted-foreground [&_li[data-checked=true]>div>p]:line-through\"\n];\n\n/*\n * Ô tick vẽ bằng CSS trên chính `<input>` (`appearance-none`), dấu tick là một chữ L xoay 45° vẽ\n * bằng viền của `::before` — không ảnh, không SVG, không mã màu: mọi màu là token của `Checkbox`\n * (`--choice-edge` viền nghỉ, `--control-on` nền khi chọn, `--control-check` dấu tick).\n */\nconst TASK_CHECKBOX = [\n \"[&_[data-type=taskList]>li>label>input]:relative [&_[data-type=taskList]>li>label>input]:m-0 [&_[data-type=taskList]>li>label>input]:grid\",\n \"[&_[data-type=taskList]>li>label>input]:size-4 [&_[data-type=taskList]>li>label>input]:shrink-0 [&_[data-type=taskList]>li>label>input]:appearance-none\",\n \"[&_[data-type=taskList]>li>label>input]:place-content-center [&_[data-type=taskList]>li>label>input]:rounded-[4px]\",\n \"[&_[data-type=taskList]>li>label>input]:border [&_[data-type=taskList]>li>label>input]:border-choice-edge\",\n \"[&_[data-type=taskList]>li>label>input]:bg-transparent [&_[data-type=taskList]>li>label>input]:transition-colors\",\n \"[&_[data-type=taskList]>li>label>input:checked]:border-control-on [&_[data-type=taskList]>li>label>input:checked]:bg-control-on\",\n \"[&_[data-type=taskList]>li>label>input:focus-visible]:outline-2 [&_[data-type=taskList]>li>label>input:focus-visible]:outline-offset-2\",\n \"[&_[data-type=taskList]>li>label>input:focus-visible]:outline-ring\",\n // Dấu tick CHỈ tồn tại khi đã tick (`content` chỉ có ở `:checked`) — không ẩn bằng `opacity-0`: một\n // dấu vô hình vẫn là một nét mà cổng màu phải đo, và đo ra 1,00:1.\n \"[&_[data-type=taskList]>li>label>input:checked]:before:h-2 [&_[data-type=taskList]>li>label>input:checked]:before:w-1\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:-translate-y-px [&_[data-type=taskList]>li>label>input:checked]:before:rotate-45\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:border-r-2 [&_[data-type=taskList]>li>label>input:checked]:before:border-b-2\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:border-control-check\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:content-['']\"\n];\n\n/*\n * Trích dẫn: vạch trái bằng `--muted-foreground` ĐẶC. `--border` đúng vai trò trang trí nhưng 1,16:1\n * ở light — gần như vô hình, trích dẫn chỉ còn là một đoạn chữ xám thụt vào. Bản `/40` dễ nhìn hơn\n * nhưng 1,71:1 trên `--muted` (light) / 1,87:1 (dark): cổng chặn ở bảng tương phản cao. Bản đặc qua\n * 3:1 ở cả bốn tổ hợp bảng màu × theme (đo bằng `pnpm lint:a11y`).\n */\nconst BLOCKS = [\n \"[&_blockquote]:border-l-2 [&_blockquote]:border-muted-foreground [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground\",\n \"[&_blockquote>*+*]:mt-2\",\n \"[&_hr]:my-1 [&_hr]:border-border\",\n // Khối mã XUỐNG DÒNG (`pre-wrap`) thay vì cuộn ngang: trong trình soạn, một thanh cuộn ngang lồng\n // trong khung cuộn dọc là chỗ con trỏ biến mất khỏi tầm nhìn khi gõ. Đó cũng là CSS Tiptap chèn sẵn\n // (`.ProseMirror pre`) — bản trước ghi `whitespace-pre` ở đây, và nó thua luật đó mà không ai thấy.\n \"[&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:px-4 [&_pre]:py-3\",\n \"[&_pre]:font-mono [&_pre]:text-[0.8125rem] [&_pre]:leading-relaxed [&_pre]:whitespace-pre-wrap\",\n \"[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-inherit\"\n];\n\n/*\n * Tô màu cú pháp (lớp `hljs-*` do lowlight sinh). Phân vai kiểu bảng màu sáng quen thuộc của các\n * trình xem mã: từ khoá đỏ, chuỗi xanh đậm, hằng/số xanh ngọc, tên hàm cam, chú thích nhạt + nghiêng.\n * Toàn bậc `-ink`, nên đổi sang dark hay bảng tương phản cao là token tự đổi theo.\n */\nconst SYNTAX = [\n \"[&_.hljs-comment]:text-muted-foreground [&_.hljs-comment]:italic [&_.hljs-quote]:text-muted-foreground\",\n \"[&_.hljs-keyword]:text-red-ink [&_.hljs-selector-tag]:text-red-ink [&_.hljs-doctag]:text-red-ink\",\n \"[&_.hljs-meta_.hljs-keyword]:text-red-ink [&_.hljs-template-tag]:text-red-ink\",\n \"[&_.hljs-string]:text-navy-ink [&_.hljs-regexp]:text-navy-ink [&_.hljs-meta_.hljs-string]:text-navy-ink\",\n \"[&_.hljs-number]:text-teal-ink [&_.hljs-literal]:text-teal-ink [&_.hljs-attr]:text-teal-ink\",\n \"[&_.hljs-attribute]:text-teal-ink [&_.hljs-variable]:text-teal-ink [&_.hljs-template-variable]:text-teal-ink\",\n \"[&_.hljs-selector-attr]:text-teal-ink [&_.hljs-selector-class]:text-teal-ink [&_.hljs-selector-id]:text-teal-ink\",\n \"[&_.hljs-title]:text-orange-ink [&_.hljs-section]:text-orange-ink [&_.hljs-name]:text-orange-ink\",\n \"[&_.hljs-type]:text-orange-ink [&_.hljs-built_in]:text-orange-ink [&_.hljs-symbol]:text-orange-ink\",\n \"[&_.hljs-meta]:text-muted-foreground [&_.hljs-bullet]:text-orange-ink [&_.hljs-link]:underline\",\n \"[&_.hljs-addition]:text-success-ink [&_.hljs-deletion]:text-destructive-ink\",\n \"[&_.hljs-emphasis]:italic [&_.hljs-strong]:font-semibold\"\n];\n\n/*\n * Bảng: ô bảng chỉ chứa MỘT đoạn văn (xem `ComitorTableCell`), nên không cần luật cho khối lồng.\n * Cuộn ngang ở `.tableWrapper` (Tiptap bọc sẵn) — bảng rộng không được đẩy cả trang tràn ngang ở 375px.\n */\nconst TABLES = [\n \"[&_.tableWrapper]:overflow-x-auto\",\n \"[&_table]:w-full [&_table]:border-collapse [&_table]:text-left\",\n \"[&_td]:border [&_td]:border-border [&_td]:px-2.5 [&_td]:py-1.5 [&_td]:align-top\",\n \"[&_th]:border [&_th]:border-border [&_th]:bg-muted [&_th]:px-2.5 [&_th]:py-1.5 [&_th]:align-top [&_th]:font-medium\",\n \"[&_td>p]:my-0 [&_th>p]:my-0 [&_.selectedCell]:bg-accent [&_.selectedCell]:text-accent-foreground\"\n];\n\nconst MEDIA = [\n \"[&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-md [&_img]:border [&_img]:border-border\",\n // Ảnh đang tải lên — một widget decoration, không phải node (xem `image-upload.ts`): đứng thành\n // một khối riêng, mờ và nhấp nháy tới khi ảnh thật được chèn vào chỗ nó.\n \"[&_[data-upload-preview]]:my-2 [&_[data-upload-preview]]:block [&_[data-upload-preview]]:w-fit\",\n \"[&_[data-upload-preview]>img]:animate-pulse [&_[data-upload-preview]>img]:opacity-60\",\n // Node được chọn bằng bấm (ảnh, đường kẻ): viền focus của gói, không phải nền.\n \"[&_.ProseMirror-selectednode]:outline-2 [&_.ProseMirror-selectednode]:outline-offset-2 [&_.ProseMirror-selectednode]:outline-ring\"\n];\n\n/*\n * Chữ mờ khi tài liệu rỗng — `Placeholder` gắn `.is-editor-empty` + `data-placeholder` lên đoạn\n * đầu. Đoạn đang gõ dở sau `/` hoặc `@` mang `.suggestion`: tô nền nhẹ để thấy chữ nào đang là lệnh.\n * Con trỏ khe (`gapcursor`, giữa hai khối không gõ được như bảng/ảnh): CSS mặc định Tiptap chèn\n * là viền `black` — vô hình ở dark, nên đổi sang `--foreground`.\n */\nconst AFFORDANCES = [\n \"[&_.is-editor-empty:first-child]:before:pointer-events-none [&_.is-editor-empty:first-child]:before:float-left\",\n \"[&_.is-editor-empty:first-child]:before:h-0 [&_.is-editor-empty:first-child]:before:text-muted-foreground\",\n \"[&_.is-editor-empty:first-child]:before:content-[attr(data-placeholder)]\",\n \"[&_.suggestion]:rounded-sm [&_.suggestion]:bg-muted\",\n \"[&_.ProseMirror-gapcursor]:after:border-foreground\"\n];\n\n/** Toàn bộ lớp của phần tử gốc ProseMirror (`.tiptap`). */\nexport const MARKDOWN_EDITOR_CONTENT_CLASS = [\n ...LAYOUT,\n ...HEADINGS,\n ...INLINE,\n ...LISTS,\n ...TASK_CHECKBOX,\n ...BLOCKS,\n ...SYNTAX,\n ...TABLES,\n ...MEDIA,\n ...AFFORDANCES\n].join(\" \");\n"],"mappings":"AAkBA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,WAAW;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,SAAS;AAAA,EACb;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AACF;AASA,MAAM,QAAQ;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AAOA,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AACF;AAOA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,QAAQ;AAAA,EACZ;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AAQA,MAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,gCAAgC;AAAA,EAC3C,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL,EAAE,KAAK,GAAG;","names":[]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nhãn của `MarkdownEditor` — file RIÊNG, không `"use client"`.
|
|
3
|
+
*
|
|
4
|
+
* Cùng khuôn `file-upload-constants.ts`: `verify:directives` chặn hằng dữ liệu thuần nằm trong file
|
|
5
|
+
* `"use client"` (Server Component đọc nó sẽ ra `undefined`, không lỗi, không cảnh báo). File này
|
|
6
|
+
* KHÔNG được nhập `@tiptap/*`: namespace `markdownEditor` của `src/i18n` nhập KIỂU từ đây, và i18n
|
|
7
|
+
* là tầng của entry CHÍNH — một import chạy được tới peer tuỳ chọn ở đây là kéo Tiptap vào mọi app.
|
|
8
|
+
*
|
|
9
|
+
* Chữ theo `ComitorLocaleProvider` (namespace `markdownEditor`, mặc định tiếng Anh, bảng tiếng Việt
|
|
10
|
+
* dựng sẵn); override từng khoá qua prop `labels`.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Các khối chèn được bằng menu `/`. Cũng là khoá của `slashKeywords` — thêm một khối mà quên từ khoá
|
|
14
|
+
* là typecheck đỏ, không phải một khối mà gõ tiếng Việt không bao giờ tìm ra.
|
|
15
|
+
*
|
|
16
|
+
* Năm khối `table*` cuối chỉ hiện khi con trỏ ĐANG ở trong bảng: chúng là thao tác trên bảng có sẵn,
|
|
17
|
+
* không phải khối mới — nhưng menu `/` là chỗ duy nhất có trong tay khi con trỏ đứng ở một ô.
|
|
18
|
+
*/
|
|
19
|
+
type MarkdownEditorBlockId = "text" | "heading1" | "heading2" | "heading3" | "bulletList" | "orderedList" | "taskList" | "blockquote" | "codeBlock" | "table" | "divider" | "image" | "tableAddRow" | "tableAddColumn" | "tableDeleteRow" | "tableDeleteColumn" | "tableDelete";
|
|
20
|
+
interface MarkdownEditorLabels {
|
|
21
|
+
/** Chữ mờ khi tài liệu rỗng — prop `placeholder` thắng */
|
|
22
|
+
placeholder: string;
|
|
23
|
+
/**
|
|
24
|
+
* Tên trợ năng của vùng soạn khi app KHÔNG đặt nhãn nào. Thứ tự: `aria-label` / `aria-labelledby`
|
|
25
|
+
* của app → `<label for={id}>` tìm được trong trang → khoá này.
|
|
26
|
+
*/
|
|
27
|
+
editorLabel: string;
|
|
28
|
+
/** Tên của thanh định dạng nổi (`role="toolbar"`) */
|
|
29
|
+
toolbar: string;
|
|
30
|
+
bold: string;
|
|
31
|
+
italic: string;
|
|
32
|
+
strike: string;
|
|
33
|
+
code: string;
|
|
34
|
+
link: string;
|
|
35
|
+
text: string;
|
|
36
|
+
heading1: string;
|
|
37
|
+
heading2: string;
|
|
38
|
+
heading3: string;
|
|
39
|
+
bulletList: string;
|
|
40
|
+
orderedList: string;
|
|
41
|
+
taskList: string;
|
|
42
|
+
blockquote: string;
|
|
43
|
+
codeBlock: string;
|
|
44
|
+
table: string;
|
|
45
|
+
divider: string;
|
|
46
|
+
image: string;
|
|
47
|
+
tableAddRow: string;
|
|
48
|
+
tableAddColumn: string;
|
|
49
|
+
tableDeleteRow: string;
|
|
50
|
+
tableDeleteColumn: string;
|
|
51
|
+
tableDelete: string;
|
|
52
|
+
/** Tên của danh sách khối (`role="listbox"`) mở bằng `/` */
|
|
53
|
+
slashMenu: string;
|
|
54
|
+
/** Khi chữ gõ sau `/` không khớp khối nào */
|
|
55
|
+
slashEmpty: string;
|
|
56
|
+
/**
|
|
57
|
+
* Từ đồng nghĩa để TÌM khối, cách nhau bằng dấu cách. So khớp không dấu, không phân biệt hoa
|
|
58
|
+
* thường (cùng luật `Combobox`), cộng với tên khối và bí danh máy tiếng Anh (`h1`, `ul`, `todo`…)
|
|
59
|
+
* luôn có sẵn — nên bảng tiếng Việt chỉ cần những chữ người Việt thật sự gõ ("gạch đầu dòng").
|
|
60
|
+
*/
|
|
61
|
+
slashKeywords: Record<MarkdownEditorBlockId, string>;
|
|
62
|
+
/** Nhãn (sr-only) của ô nhập địa chỉ liên kết */
|
|
63
|
+
linkInput: string;
|
|
64
|
+
linkPlaceholder: string;
|
|
65
|
+
linkApply: string;
|
|
66
|
+
linkEdit: string;
|
|
67
|
+
linkOpen: string;
|
|
68
|
+
linkRemove: string;
|
|
69
|
+
/** Khi địa chỉ không phải `http(s)` / `mailto` / `tel` — gói từ chối `javascript:` và họ hàng */
|
|
70
|
+
linkInvalid: string;
|
|
71
|
+
/** Tên của danh sách người (`role="listbox"`) mở bằng `@` */
|
|
72
|
+
mentionMenu: string;
|
|
73
|
+
mentionEmpty: string;
|
|
74
|
+
mentionLoading: string;
|
|
75
|
+
/** Tên trợ năng của ảnh đang tải lên (widget `role="img"` hiện tới khi app trả `src`) */
|
|
76
|
+
imageUploading: string;
|
|
77
|
+
/** Toast mặc định khi `onImageUpload` ném lỗi mà app không truyền `onImageUploadError` */
|
|
78
|
+
imageUploadFailed: string;
|
|
79
|
+
/**
|
|
80
|
+
* Tên trợ năng của ô tick trong danh sách việc. HÀM vì nó chèn nội dung của chính mục đó — trật tự
|
|
81
|
+
* từ là việc của từng ngôn ngữ. `text` rỗng khi mục chưa có chữ.
|
|
82
|
+
*/
|
|
83
|
+
taskCheckbox: (text: string) => string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Nhãn TIẾNG ANH mặc định của `MarkdownEditor`. Bảng tiếng Việt: `comitorMessagesVi.markdownEditor`.
|
|
87
|
+
* Component không đọc thẳng hằng này — nó đọc bảng của `ComitorLocaleProvider`; hằng ở đây để app
|
|
88
|
+
* trải ra khi tự dựng nhãn.
|
|
89
|
+
*/
|
|
90
|
+
declare const DEFAULT_MARKDOWN_EDITOR_LABELS: MarkdownEditorLabels;
|
|
91
|
+
|
|
92
|
+
export { DEFAULT_MARKDOWN_EDITOR_LABELS, type MarkdownEditorBlockId, type MarkdownEditorLabels };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/editor/markdown-editor-constants.ts"],"sourcesContent":["/**\n * Nhãn của `MarkdownEditor` — file RIÊNG, không `\"use client\"`.\n *\n * Cùng khuôn `file-upload-constants.ts`: `verify:directives` chặn hằng dữ liệu thuần nằm trong file\n * `\"use client\"` (Server Component đọc nó sẽ ra `undefined`, không lỗi, không cảnh báo). File này\n * KHÔNG được nhập `@tiptap/*`: namespace `markdownEditor` của `src/i18n` nhập KIỂU từ đây, và i18n\n * là tầng của entry CHÍNH — một import chạy được tới peer tuỳ chọn ở đây là kéo Tiptap vào mọi app.\n *\n * Chữ theo `ComitorLocaleProvider` (namespace `markdownEditor`, mặc định tiếng Anh, bảng tiếng Việt\n * dựng sẵn); override từng khoá qua prop `labels`.\n */\n\nimport { markdownEditorEn } from \"../i18n/namespaces/markdown-editor.js\";\n\n/**\n * Các khối chèn được bằng menu `/`. Cũng là khoá của `slashKeywords` — thêm một khối mà quên từ khoá\n * là typecheck đỏ, không phải một khối mà gõ tiếng Việt không bao giờ tìm ra.\n *\n * Năm khối `table*` cuối chỉ hiện khi con trỏ ĐANG ở trong bảng: chúng là thao tác trên bảng có sẵn,\n * không phải khối mới — nhưng menu `/` là chỗ duy nhất có trong tay khi con trỏ đứng ở một ô.\n */\nexport type MarkdownEditorBlockId =\n | \"text\"\n | \"heading1\"\n | \"heading2\"\n | \"heading3\"\n | \"bulletList\"\n | \"orderedList\"\n | \"taskList\"\n | \"blockquote\"\n | \"codeBlock\"\n | \"table\"\n | \"divider\"\n | \"image\"\n | \"tableAddRow\"\n | \"tableAddColumn\"\n | \"tableDeleteRow\"\n | \"tableDeleteColumn\"\n | \"tableDelete\";\n\nexport interface MarkdownEditorLabels {\n /** Chữ mờ khi tài liệu rỗng — prop `placeholder` thắng */\n placeholder: string;\n /**\n * Tên trợ năng của vùng soạn khi app KHÔNG đặt nhãn nào. Thứ tự: `aria-label` / `aria-labelledby`\n * của app → `<label for={id}>` tìm được trong trang → khoá này.\n */\n editorLabel: string;\n\n /** Tên của thanh định dạng nổi (`role=\"toolbar\"`) */\n toolbar: string;\n bold: string;\n italic: string;\n strike: string;\n code: string;\n link: string;\n\n /*\n * Tên khối — dùng CHUNG cho nút của thanh định dạng và mục của menu `/`: cùng một thao tác thì\n * cùng một tên ở mọi chỗ, nên không có cặp khoá \"toolbarHeading1\" / \"slashHeading1\" để lệch nhau.\n */\n text: string;\n heading1: string;\n heading2: string;\n heading3: string;\n bulletList: string;\n orderedList: string;\n taskList: string;\n blockquote: string;\n codeBlock: string;\n table: string;\n divider: string;\n image: string;\n tableAddRow: string;\n tableAddColumn: string;\n tableDeleteRow: string;\n tableDeleteColumn: string;\n tableDelete: string;\n\n /** Tên của danh sách khối (`role=\"listbox\"`) mở bằng `/` */\n slashMenu: string;\n /** Khi chữ gõ sau `/` không khớp khối nào */\n slashEmpty: string;\n /**\n * Từ đồng nghĩa để TÌM khối, cách nhau bằng dấu cách. So khớp không dấu, không phân biệt hoa\n * thường (cùng luật `Combobox`), cộng với tên khối và bí danh máy tiếng Anh (`h1`, `ul`, `todo`…)\n * luôn có sẵn — nên bảng tiếng Việt chỉ cần những chữ người Việt thật sự gõ (\"gạch đầu dòng\").\n */\n slashKeywords: Record<MarkdownEditorBlockId, string>;\n\n /** Nhãn (sr-only) của ô nhập địa chỉ liên kết */\n linkInput: string;\n linkPlaceholder: string;\n linkApply: string;\n linkEdit: string;\n linkOpen: string;\n linkRemove: string;\n /** Khi địa chỉ không phải `http(s)` / `mailto` / `tel` — gói từ chối `javascript:` và họ hàng */\n linkInvalid: string;\n\n /** Tên của danh sách người (`role=\"listbox\"`) mở bằng `@` */\n mentionMenu: string;\n mentionEmpty: string;\n mentionLoading: string;\n\n /** Tên trợ năng của ảnh đang tải lên (widget `role=\"img\"` hiện tới khi app trả `src`) */\n imageUploading: string;\n /** Toast mặc định khi `onImageUpload` ném lỗi mà app không truyền `onImageUploadError` */\n imageUploadFailed: string;\n\n /**\n * Tên trợ năng của ô tick trong danh sách việc. HÀM vì nó chèn nội dung của chính mục đó — trật tự\n * từ là việc của từng ngôn ngữ. `text` rỗng khi mục chưa có chữ.\n */\n taskCheckbox: (text: string) => string;\n}\n\n/**\n * Nhãn TIẾNG ANH mặc định của `MarkdownEditor`. Bảng tiếng Việt: `comitorMessagesVi.markdownEditor`.\n * Component không đọc thẳng hằng này — nó đọc bảng của `ComitorLocaleProvider`; hằng ở đây để app\n * trải ra khi tự dựng nhãn.\n */\nexport const DEFAULT_MARKDOWN_EDITOR_LABELS: MarkdownEditorLabels = { ...markdownEditorEn };\n"],"mappings":"AAYA,SAAS,wBAAwB;AA8G1B,MAAM,iCAAuD,EAAE,GAAG,iBAAiB;","names":[]}
|