@comitor/ui 3.11.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -5
- package/dist/chat/chat-composer.d.ts +36 -0
- package/dist/chat/chat-composer.js +85 -0
- package/dist/chat/chat-composer.js.map +1 -0
- package/dist/chat/chat-constants.d.ts +63 -0
- package/dist/chat/chat-constants.js +1 -0
- package/dist/chat/chat-constants.js.map +1 -0
- package/dist/chat/chat-message.d.ts +51 -0
- package/dist/chat/chat-message.js +137 -0
- package/dist/chat/chat-message.js.map +1 -0
- package/dist/chat/chat-sources.d.ts +25 -0
- package/dist/chat/chat-sources.js +97 -0
- package/dist/chat/chat-sources.js.map +1 -0
- package/dist/chat/chat-steps.d.ts +20 -0
- package/dist/chat/chat-steps.js +84 -0
- package/dist/chat/chat-steps.js.map +1 -0
- package/dist/chat/chat-thread.d.ts +21 -0
- package/dist/chat/chat-thread.js +87 -0
- package/dist/chat/chat-thread.js.map +1 -0
- package/dist/chat/index.d.ts +12 -0
- package/dist/chat/index.js +14 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/components/composite/gantt-constants.d.ts +127 -0
- package/dist/components/composite/gantt-constants.js +5 -0
- package/dist/components/composite/gantt-constants.js.map +1 -0
- package/dist/components/composite/gantt-utils.d.ts +106 -0
- package/dist/components/composite/gantt-utils.js +196 -0
- package/dist/components/composite/gantt-utils.js.map +1 -0
- package/dist/components/composite/gantt.d.ts +93 -0
- package/dist/components/composite/gantt.js +1137 -0
- package/dist/components/composite/gantt.js.map +1 -0
- package/dist/components/composite/stat-card.d.ts +1 -1
- package/dist/components/composite/tree-view-constants.d.ts +96 -0
- package/dist/components/composite/tree-view-constants.js +85 -0
- package/dist/components/composite/tree-view-constants.js.map +1 -0
- package/dist/components/composite/tree-view.d.ts +58 -0
- package/dist/components/composite/tree-view.js +385 -0
- package/dist/components/composite/tree-view.js.map +1 -0
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/button.d.ts +1 -1
- package/dist/data-grid/data-grid-cell-wrapper.d.ts +1 -1
- package/dist/data-grid/data-grid-cell-wrapper.js +11 -5
- package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -1
- package/dist/data-grid/data-grid-column-header.js +12 -12
- package/dist/data-grid/data-grid-column-header.js.map +1 -1
- package/dist/data-grid/messages-en.d.ts +2 -0
- package/dist/data-grid/messages-en.js +3 -1
- package/dist/data-grid/messages-en.js.map +1 -1
- package/dist/data-grid/messages-vi.js +2 -1
- package/dist/data-grid/messages-vi.js.map +1 -1
- package/dist/data-grid/messages.d.ts +2 -0
- package/dist/data-grid/use-data-grid.d.ts +1 -0
- package/dist/data-grid/utils.d.ts +13 -1
- package/dist/data-grid/utils.js +13 -0
- package/dist/data-grid/utils.js.map +1 -1
- package/dist/editor/bubble-toolbar.d.ts +1 -1
- package/dist/editor/editor-core.d.ts +2 -2
- package/dist/editor/editor-core.js +6 -0
- package/dist/editor/editor-core.js.map +1 -1
- package/dist/editor/editor-types.d.ts +1 -1
- package/dist/editor/fixed-toolbar.d.ts +1 -1
- package/dist/editor/image-upload.d.ts +1 -1
- package/dist/editor/index.d.ts +1 -1
- package/dist/editor/markdown-editor.d.ts +1 -1
- package/dist/editor/markdown-extensions.d.ts +3 -1
- package/dist/editor/markdown-extensions.js +3 -1
- package/dist/editor/markdown-extensions.js.map +1 -1
- package/dist/editor/rich-text-editor.d.ts +1 -1
- package/dist/editor/slash-commands.d.ts +1 -1
- package/dist/editor/toolbar-actions.d.ts +1 -1
- package/dist/{editor-types-CSrMeuSw.d.ts → editor-types-BacXD2VN.d.ts} +7 -1
- package/dist/i18n/locale-provider.d.ts +3 -0
- package/dist/i18n/messages.d.ts +6 -0
- package/dist/i18n/messages.js +11 -2
- package/dist/i18n/messages.js.map +1 -1
- package/dist/i18n/namespaces/chat.d.ts +8 -0
- package/dist/i18n/namespaces/chat.js +47 -0
- package/dist/i18n/namespaces/chat.js.map +1 -0
- package/dist/i18n/namespaces/gantt.d.ts +10 -0
- package/dist/i18n/namespaces/gantt.js +57 -0
- package/dist/i18n/namespaces/gantt.js.map +1 -0
- package/dist/i18n/namespaces/tree-view.d.ts +8 -0
- package/dist/i18n/namespaces/tree-view.js +21 -0
- package/dist/i18n/namespaces/tree-view.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +9 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/data-grid/utils.ts"],"sourcesContent":["import type { Column, Table } from \"@tanstack/react-table\";\nimport { type Locale as DateFnsLocale, format, isValid, parse } from \"date-fns\";\nimport {\n BaselineIcon,\n CalendarIcon,\n CheckSquareIcon,\n File,\n FileArchive,\n FileAudio,\n FileIcon,\n FileImage,\n FileSpreadsheet,\n FileText,\n FileVideo,\n HashIcon,\n LinkIcon,\n ListChecksIcon,\n ListIcon,\n Presentation,\n WrapTextIcon\n} from \"lucide-react\";\nimport type * as React from \"react\";\nimport { type DataGridMessages, getDataGridMessages } from \"./messages.js\";\nimport type { CellOpts, CellPosition, Direction, FileCellData, RowHeightValue } from \"./types.js\";\n\nexport function flexRender<TProps extends object>(\n Comp: ((props: TProps) => React.ReactNode) | string | undefined,\n props: TProps\n): React.ReactNode {\n if (typeof Comp === \"string\") {\n return Comp;\n }\n return Comp?.(props);\n}\n\nexport function getIsFileCellData(item: unknown): item is FileCellData {\n return !!item && typeof item === \"object\" && \"id\" in item && \"name\" in item && \"size\" in item && \"type\" in item;\n}\n\nexport function matchSelectOption(value: string, options: { value: string; label: string }[]): string | undefined {\n return options.find(\n (o) =>\n o.value === value ||\n o.value.toLowerCase() === value.toLowerCase() ||\n o.label.toLowerCase() === value.toLowerCase()\n )?.value;\n}\n\nexport function getCellKey(rowIndex: number, columnId: string) {\n return `${rowIndex}:${columnId}`;\n}\n\nexport function parseCellKey(cellKey: string): Required<CellPosition> {\n const parts = cellKey.split(\":\");\n const rowIndexStr = parts[0];\n const columnId = parts[1];\n if (rowIndexStr && columnId) {\n const rowIndex = parseInt(rowIndexStr, 10);\n if (!Number.isNaN(rowIndex)) {\n return { rowIndex, columnId };\n }\n }\n return { rowIndex: 0, columnId: \"\" };\n}\n\nexport function getRowHeightValue(rowHeight: RowHeightValue): number {\n const rowHeightMap: Record<RowHeightValue, number> = {\n short: 36,\n medium: 56,\n tall: 76,\n \"extra-tall\": 96\n };\n\n return rowHeightMap[rowHeight];\n}\n\nexport function getLineCount(rowHeight: RowHeightValue): number {\n const lineCountMap: Record<RowHeightValue, number> = {\n short: 1,\n medium: 2,\n tall: 3,\n \"extra-tall\": 4\n };\n\n return lineCountMap[rowHeight];\n}\n\nexport function getColumnBorderVisibility<TData>(params: {\n column: Column<TData>;\n nextColumn?: Column<TData>;\n isLastColumn: boolean;\n}): {\n showEndBorder: boolean;\n showStartBorder: boolean;\n} {\n const { column, nextColumn, isLastColumn } = params;\n\n const isPinned = column.getIsPinned();\n const isFirstRightPinnedColumn = isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n const isLastRightPinnedColumn = isPinned === \"right\" && column.getIsLastColumn(\"right\");\n\n const nextIsPinned = nextColumn?.getIsPinned();\n const isBeforeRightPinned = nextIsPinned === \"right\" && nextColumn?.getIsFirstColumn(\"right\");\n\n const showEndBorder = !isBeforeRightPinned && (isLastColumn || !isLastRightPinnedColumn);\n\n const showStartBorder = isFirstRightPinnedColumn;\n\n return {\n showEndBorder,\n showStartBorder\n };\n}\n\nexport function getColumnPinningStyle<TData>(params: {\n column: Column<TData>;\n withBorder?: boolean;\n dir?: Direction;\n}): React.CSSProperties {\n const { column, dir = \"ltr\", withBorder = false } = params;\n\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn = isPinned === \"left\" && column.getIsLastColumn(\"left\");\n const isFirstRightPinnedColumn = isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n const isRtl = dir === \"rtl\";\n\n const leftPosition = isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined;\n const rightPosition = isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined;\n\n /*\n * ⚠ Token của gói này là MÀU ĐẦY ĐỦ (`--card: #ffffff`), KHÔNG phải bộ 3 kênh HSL — nên viết\n * `var(--card)` trần, đừng bọc `hsl()`. Bản gốc port từ một hệ token kiểu `\"150 20% 98%\"` nên\n * ở đó `hsl(var(--card))` mới đúng; bê nguyên sang đây thì `hsl(#ffffff)` là giá trị KHÔNG\n * HỢP LỆ, và hệ quả là cột ghim trong suốt — nội dung cuộn phía dưới lộ xuyên qua. Không có\n * lỗi nào được ném ra: CSS bỏ qua khai báo hỏng rồi rơi về `background: transparent`.\n *\n * Nền lấy theo `--card` vì hộp lưới dùng `bg-card` như mọi bảng khác của gói.\n *\n * ⚠ Khác bản gốc (3.3.2): KHÔNG có `opacity`. Bản gốc đặt `opacity: 0.97` cho ô ghim — nền đã\n * đục nhưng cả Ô mờ 3%, nên khi cuộn ngang, chữ của cột đang trôi phía dưới lộ thành vệt mờ sau\n * cột ghim. Cùng một kết cục với lỗi `hsl()` ngay trên (cột ghim để lộ nội dung cuộn qua), chỉ\n * nhạt hơn — và cũng không cổng nào thấy, vì `opacity` nằm ở `style` inline. `test:grid-render`\n * nay canh cả chuyện này. Ranh giới vùng ghim ⇄ vùng cuộn đã có viền cuối cột lo.\n */\n return {\n boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? isRtl\n ? \"4px 0 4px -4px var(--border) inset\"\n : \"-4px 0 4px -4px var(--border) inset\"\n : isFirstRightPinnedColumn\n ? isRtl\n ? \"-4px 0 4px -4px var(--border) inset\"\n : \"4px 0 4px -4px var(--border) inset\"\n : undefined\n : undefined,\n left: isRtl ? rightPosition : leftPosition,\n right: isRtl ? leftPosition : rightPosition,\n position: isPinned ? \"sticky\" : \"relative\",\n background: \"var(--card)\",\n width: column.getSize(),\n zIndex: isPinned ? 1 : undefined\n };\n}\n\nexport function getScrollDirection(direction: string): \"left\" | \"right\" | \"home\" | \"end\" | undefined {\n if (direction === \"left\" || direction === \"right\" || direction === \"home\" || direction === \"end\") {\n return direction as \"left\" | \"right\" | \"home\" | \"end\";\n }\n if (direction === \"pageleft\") return \"left\";\n if (direction === \"pageright\") return \"right\";\n return undefined;\n}\n\nexport function scrollCellIntoView<TData>(params: {\n container: HTMLDivElement;\n targetCell: HTMLDivElement;\n tableRef: React.RefObject<Table<TData> | null>;\n viewportOffset: number;\n direction?: \"left\" | \"right\" | \"home\" | \"end\";\n isRtl: boolean;\n}): void {\n const { container, targetCell, tableRef, direction, viewportOffset, isRtl } = params;\n\n const containerRect = container.getBoundingClientRect();\n const cellRect = targetCell.getBoundingClientRect();\n\n const hasNegativeScroll = container.scrollLeft < 0;\n const isActuallyRtl = isRtl || hasNegativeScroll;\n\n const currentTable = tableRef.current;\n const leftPinnedColumns = currentTable?.getLeftVisibleLeafColumns() ?? [];\n const rightPinnedColumns = currentTable?.getRightVisibleLeafColumns() ?? [];\n\n const leftPinnedWidth = leftPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n const rightPinnedWidth = rightPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n\n const viewportLeft = isActuallyRtl\n ? containerRect.left + rightPinnedWidth + viewportOffset\n : containerRect.left + leftPinnedWidth + viewportOffset;\n const viewportRight = isActuallyRtl\n ? containerRect.right - leftPinnedWidth - viewportOffset\n : containerRect.right - rightPinnedWidth - viewportOffset;\n\n const isFullyVisible = cellRect.left >= viewportLeft && cellRect.right <= viewportRight;\n\n if (isFullyVisible) return;\n\n const isClippedLeft = cellRect.left < viewportLeft;\n const isClippedRight = cellRect.right > viewportRight;\n\n let scrollDelta = 0;\n\n if (!direction) {\n if (isClippedRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else if (isClippedLeft) {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n } else {\n const shouldScrollRight = isActuallyRtl\n ? direction === \"right\" || direction === \"home\"\n : direction === \"right\" || direction === \"end\";\n\n if (shouldScrollRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n }\n\n container.scrollLeft += scrollDelta;\n}\n\nfunction countTabs(s: string): number {\n let n = 0;\n for (let i = 0; i < s.length; i++) if (s[i] === \"\\t\") n++;\n return n;\n}\n\nexport function parseTsv(text: string, fallbackColumnCount: number): string[][] {\n if (text.startsWith('\"') || text.includes('\\t\"')) {\n const rows: string[][] = [];\n let currentRow: string[] = [];\n let currentField = \"\";\n let inQuotes = false;\n let i = 0;\n\n while (i < text.length) {\n const char = text[i];\n const nextChar = text[i + 1];\n\n if (inQuotes) {\n if (char === '\"' && nextChar === '\"') {\n currentField += '\"';\n i += 2;\n } else if (char === '\"') {\n inQuotes = false;\n i++;\n } else {\n currentField += char;\n i++;\n }\n } else {\n if (char === '\"' && currentField === \"\") {\n inQuotes = true;\n i++;\n } else if (char === \"\\t\") {\n currentRow.push(currentField);\n currentField = \"\";\n i++;\n } else if (char === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i++;\n } else if (char === \"\\r\" && nextChar === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i += 2;\n } else {\n currentField += char;\n i++;\n }\n }\n }\n\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n\n return rows;\n }\n\n const lines = text.split(\"\\n\").map((l) => l.replace(/\\r$/, \"\"));\n let maxTabCount = 0;\n for (const line of lines) {\n const n = countTabs(line);\n if (n > maxTabCount) maxTabCount = n;\n }\n const columnCount = maxTabCount > 0 ? maxTabCount + 1 : fallbackColumnCount;\n if (columnCount <= 0) return [];\n\n const expectedTabCount = columnCount - 1;\n const rows: string[][] = [];\n let buf = \"\";\n let bufTabCount = 0;\n\n for (const line of lines) {\n const tc = countTabs(line);\n\n if (tc === expectedTabCount) {\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n rows.push(line.split(\"\\t\"));\n } else {\n buf = buf ? `${buf}\\n${line}` : line;\n bufTabCount += tc;\n if (bufTabCount === expectedTabCount) {\n rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n }\n }\n }\n\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n\n return rows.length > 0 ? rows : lines.filter((l) => l.length > 0).map((l) => l.split(\"\\t\"));\n}\n\n/**\n * Phần tử có đang nằm trong một lớp nổi không? Dùng để lưới KHÔNG coi thao tác\n * trong popover/menu/hộp thoại là \"bấm ra ngoài\" (sẽ mất focus ô đang sửa).\n *\n * Primitive của package không gắn `data-slot`, nên bắt theo wrapper của Radix Popper\n * (bao Popover/DropdownMenu/Select/Tooltip) và theo role — kể cả Select \"item-aligned\".\n *\n * ⚠ Thêm ở `3.1.0`: `ActionBar` của gói (`data-slot=\"action-bar\"`). Thanh thao tác hàng loạt là bạn\n * đi kèm của lưới — nó mở khi có dòng được tick — nhưng nó là `role=\"toolbar\"` nằm NGOÀI lưới,\n * nên trước đây một cú bấm vào nó bị tính là \"bấm ra ngoài\": `mousedown` xoá lựa chọn dòng, thanh\n * đóng lại TRƯỚC khi `click` tới nơi, và hành động không bao giờ chạy. Đo trên comitor-ds: tick hai\n * dòng, bấm \"Mark as done\" → thanh biến mất, không dòng nào đổi. Cùng lý do, focus bàn phím đi\n * vào thanh cũng bị lưới kéo ngược về ô.\n */\nexport function getIsInPopover(element: unknown): boolean {\n if (!(element instanceof Element)) return false;\n\n return (\n element.closest(\"[data-grid-cell-editor]\") !== null ||\n element.closest(\"[data-grid-popover]\") !== null ||\n element.closest(\"[data-radix-popper-content-wrapper]\") !== null ||\n element.closest(\"[data-slot='action-bar']\") !== null ||\n element.closest(\"[role='dialog']\") !== null ||\n // `alertdialog` (3.1.0): `ConfirmDialog` của gói là `role=\"alertdialog\"` — thiếu dòng này thì\n // bấm Huỷ trong hộp xác nhận xoá các dòng đang tick, dù hộp mở ra chính vì các dòng đó.\n element.closest(\"[role='alertdialog']\") !== null ||\n element.closest(\"[role='menu']\") !== null ||\n element.closest(\"[role='listbox']\") !== null\n );\n}\n\n/**\n * Icon + tên kiểu dữ liệu của một cột (hiện trong tooltip ở tiêu đề cột).\n *\n * `messages` bỏ trống = bảng mặc định (tiếng Anh), giữ đúng chữ ký của `1.11.0`. Component nào\n * vẽ nhãn này cho người dùng thì PHẢI truyền bảng đang có hiệu lực vào — quên là tooltip nói\n * tiếng Anh giữa một lưới tiếng Việt.\n */\nexport function getColumnVariant(\n variant?: CellOpts[\"variant\"],\n messages?: DataGridMessages\n): {\n icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;\n label: string;\n} | null {\n const msg = messages ?? getDataGridMessages();\n switch (variant) {\n case \"short-text\":\n return { label: msg.column.variant.shortText, icon: BaselineIcon };\n case \"long-text\":\n return { label: msg.column.variant.longText, icon: WrapTextIcon };\n case \"number\":\n return { label: msg.column.variant.number, icon: HashIcon };\n case \"url\":\n return { label: msg.column.variant.url, icon: LinkIcon };\n case \"checkbox\":\n return { label: msg.column.variant.checkbox, icon: CheckSquareIcon };\n case \"select\":\n return { label: msg.column.variant.select, icon: ListIcon };\n case \"multi-select\":\n return { label: msg.column.variant.multiSelect, icon: ListChecksIcon };\n case \"date\":\n return { label: msg.column.variant.date, icon: CalendarIcon };\n case \"file\":\n return { label: msg.column.variant.file, icon: FileIcon };\n default:\n return null;\n }\n}\n\nexport function getEmptyCellValue(variant: CellOpts[\"variant\"] | undefined): unknown {\n if (variant === \"multi-select\" || variant === \"file\") return [];\n if (variant === \"number\" || variant === \"date\" || variant === \"select\") return null;\n if (variant === \"checkbox\") return false;\n return \"\";\n}\n\nexport function getUrlHref(urlString: string): string {\n if (!urlString || urlString.trim() === \"\") return \"\";\n\n const trimmed = urlString.trim();\n\n // Reject dangerous protocols (extra safety, though our http:// prefix would neutralize them)\n if (/^(javascript|data|vbscript|file):/i.test(trimmed)) {\n return \"\";\n }\n\n if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n return trimmed;\n }\n\n return `http://${trimmed}`;\n}\n\nexport function parseLocalDate(dateStr: unknown): Date | null {\n if (!dateStr) return null;\n if (dateStr instanceof Date) return dateStr;\n if (typeof dateStr !== \"string\") return null;\n const [year, month, day] = dateStr.split(\"-\").map(Number);\n if (!year || !month || !day) return null;\n const date = new Date(year, month - 1, day);\n // Verify date wasn't auto-corrected (e.g. Feb 30 -> Mar 1)\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\nexport function formatDateToString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Mẫu định dạng ngày của lưới: ngày NGẮN theo locale date-fns — `en-US` ra `12/25/2024`,\n * `vi` / `en-GB` ra `25/12/2024`. Một mẫu cho cả hiển thị lẫn phép đọc lại ({@link parseDateInput}),\n * để thứ người dùng THẤY cũng là thứ dán vào được.\n */\nconst DATE_DISPLAY_FORMAT = \"P\";\n\n/**\n * Hiển thị ngày dạng ngắn theo `locale` (dùng cho ô ngày, bộ lọc ngày).\n *\n * Hàm THUẦN nên không đọc được ngôn ngữ của lưới: component truyền locale đang có hiệu lực vào\n * (`useDataGridDateLocale()`). Bỏ trống `locale` = locale mặc định của date-fns (`enUS`, trừ khi\n * app đã đổi bằng `setDefaultOptions`).\n *\n * ⚠ Trước `3.0.0` hàm này luôn ra `dd/MM/yyyy` bất kể ngôn ngữ. Cần lại đúng chuỗi cũ thì truyền\n * `vi` (`import { vi } from \"date-fns/locale\"`).\n */\nexport function formatDate(date: Date | string | number | null | undefined, locale?: DateFnsLocale): string {\n if (!date) return \"\";\n const value = date instanceof Date ? date : new Date(date);\n if (Number.isNaN(value.getTime())) return \"\";\n return format(value, DATE_DISPLAY_FORMAT, { locale });\n}\n\n/** Như {@link formatDate}, cho giá trị thô của ô ngày (`yyyy-MM-dd` hoặc `Date`). */\nexport function formatDateForDisplay(dateStr: unknown, locale?: DateFnsLocale): string {\n if (!dateStr) return \"\";\n const date = parseLocalDate(dateStr);\n if (!date) return typeof dateStr === \"string\" ? dateStr : \"\";\n return formatDate(date, locale);\n}\n\n/**\n * Đọc chuỗi ngày DÁN vào ô ngày. Thử dạng ngắn của `locale` trước — đúng dạng lưới đang hiển\n * thị, nên `25/12/2024` dán vào lưới tiếng Việt là 25 tháng 12, không phải lỗi — rồi mới tới\n * `new Date()` (ISO `yyyy-MM-dd`, và những gì JS tự hiểu). `null` khi không đọc được.\n *\n * Năm phải đủ 4 chữ số: mẫu `\"P\"` đọc `25/12/24` thành năm 24 sau Công nguyên, một ngày \"hợp lệ\"\n * mà không ai muốn — chuỗi như thế rơi xuống `new Date()` như trước.\n */\nexport function parseDateInput(text: string, locale?: DateFnsLocale): Date | null {\n const trimmed = text.trim();\n if (!trimmed) return null;\n const localized = parse(trimmed, DATE_DISPLAY_FORMAT, new Date(), { locale });\n if (isValid(localized) && localized.getFullYear() >= 1000) return localized;\n const date = new Date(trimmed);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Id cho tệp đính kèm. `crypto.randomUUID` chỉ có ở secure context (https/localhost) —\n * app nội bộ chạy http trong mạng bệnh viện sẽ không có, nên phải có đường lui.\n */\nexport function generateFileId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `file-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Chép text vào clipboard. `navigator.clipboard` cũng chỉ có ở secure context,\n * nên rơi về `document.execCommand(\"copy\")` (đã deprecated nhưng vẫn chạy mọi trình duyệt).\n * Ném lỗi khi cả hai đường đều không dùng được để phía gọi báo toast.\n */\nexport async function writeClipboardText(text: string): Promise<void> {\n if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n try {\n await navigator.clipboard.writeText(text);\n return;\n } catch {\n // Bị chặn quyền → thử đường lui bên dưới\n }\n }\n\n if (!copyWithExecCommand(text)) {\n throw new Error(\"Clipboard không khả dụng\");\n }\n}\n\nfunction copyWithExecCommand(text: string): boolean {\n if (typeof document === \"undefined\") return false;\n\n const textarea = document.createElement(\"textarea\");\n textarea.value = text;\n textarea.setAttribute(\"readonly\", \"\");\n textarea.style.position = \"fixed\";\n textarea.style.top = \"0\";\n textarea.style.insetInlineStart = \"-9999px\";\n document.body.appendChild(textarea);\n\n const previouslyFocused = document.activeElement;\n textarea.select();\n\n let copied = false;\n try {\n copied = document.execCommand(\"copy\");\n } catch {\n copied = false;\n }\n\n document.body.removeChild(textarea);\n if (previouslyFocused instanceof HTMLElement) {\n previouslyFocused.focus();\n }\n\n return copied;\n}\n\n/** Đọc clipboard khi có quyền; trả `null` nếu trình duyệt/ngữ cảnh không cho đọc. */\nexport async function readClipboardText(): Promise<string | null> {\n if (typeof navigator === \"undefined\" || !navigator.clipboard?.readText) return null;\n try {\n return await navigator.clipboard.readText();\n } catch {\n return null;\n }\n}\n\nexport function formatFileSize(bytes: number): string {\n if (bytes <= 0 || !Number.isFinite(bytes)) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(k)));\n return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n}\n\nexport function getFileIcon(type: string): React.ComponentType<React.SVGProps<SVGSVGElement>> {\n if (type.startsWith(\"image/\")) return FileImage;\n if (type.startsWith(\"video/\")) return FileVideo;\n if (type.startsWith(\"audio/\")) return FileAudio;\n if (type.includes(\"pdf\")) return FileText;\n if (type.includes(\"zip\") || type.includes(\"rar\")) return FileArchive;\n if (type.includes(\"word\") || type.includes(\"document\") || type.includes(\"doc\")) return FileText;\n if (type.includes(\"sheet\") || type.includes(\"excel\") || type.includes(\"xls\")) return FileSpreadsheet;\n if (type.includes(\"presentation\") || type.includes(\"powerpoint\") || type.includes(\"ppt\")) return Presentation;\n return File;\n}\n"],"mappings":"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","names":["rows"]}
|
|
1
|
+
{"version":3,"sources":["../../src/data-grid/utils.ts"],"sourcesContent":["import type { Column, Table } from \"@tanstack/react-table\";\nimport { type Locale as DateFnsLocale, format, isValid, parse } from \"date-fns\";\nimport {\n BaselineIcon,\n CalendarIcon,\n CheckSquareIcon,\n File,\n FileArchive,\n FileAudio,\n FileIcon,\n FileImage,\n FileSpreadsheet,\n FileText,\n FileVideo,\n HashIcon,\n LinkIcon,\n ListChecksIcon,\n ListIcon,\n Presentation,\n WrapTextIcon\n} from \"lucide-react\";\nimport type * as React from \"react\";\nimport { type DataGridMessages, getDataGridMessages } from \"./messages.js\";\nimport type { CellOpts, CellPosition, Direction, FileCellData, RowHeightValue } from \"./types.js\";\n\nexport function flexRender<TProps extends object>(\n Comp: ((props: TProps) => React.ReactNode) | string | undefined,\n props: TProps\n): React.ReactNode {\n if (typeof Comp === \"string\") {\n return Comp;\n }\n return Comp?.(props);\n}\n\nexport function getIsFileCellData(item: unknown): item is FileCellData {\n return !!item && typeof item === \"object\" && \"id\" in item && \"name\" in item && \"size\" in item && \"type\" in item;\n}\n\nexport function matchSelectOption(value: string, options: { value: string; label: string }[]): string | undefined {\n return options.find(\n (o) =>\n o.value === value ||\n o.value.toLowerCase() === value.toLowerCase() ||\n o.label.toLowerCase() === value.toLowerCase()\n )?.value;\n}\n\nexport function getCellKey(rowIndex: number, columnId: string) {\n return `${rowIndex}:${columnId}`;\n}\n\nexport function parseCellKey(cellKey: string): Required<CellPosition> {\n const parts = cellKey.split(\":\");\n const rowIndexStr = parts[0];\n const columnId = parts[1];\n if (rowIndexStr && columnId) {\n const rowIndex = parseInt(rowIndexStr, 10);\n if (!Number.isNaN(rowIndex)) {\n return { rowIndex, columnId };\n }\n }\n return { rowIndex: 0, columnId: \"\" };\n}\n\nexport function getRowHeightValue(rowHeight: RowHeightValue): number {\n const rowHeightMap: Record<RowHeightValue, number> = {\n short: 36,\n medium: 56,\n tall: 76,\n \"extra-tall\": 96\n };\n\n return rowHeightMap[rowHeight];\n}\n\nexport function getLineCount(rowHeight: RowHeightValue): number {\n const lineCountMap: Record<RowHeightValue, number> = {\n short: 1,\n medium: 2,\n tall: 3,\n \"extra-tall\": 4\n };\n\n return lineCountMap[rowHeight];\n}\n\nexport function getColumnBorderVisibility<TData>(params: {\n column: Column<TData>;\n nextColumn?: Column<TData>;\n isLastColumn: boolean;\n}): {\n showEndBorder: boolean;\n showStartBorder: boolean;\n} {\n const { column, nextColumn, isLastColumn } = params;\n\n const isPinned = column.getIsPinned();\n const isFirstRightPinnedColumn = isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n const isLastRightPinnedColumn = isPinned === \"right\" && column.getIsLastColumn(\"right\");\n\n const nextIsPinned = nextColumn?.getIsPinned();\n const isBeforeRightPinned = nextIsPinned === \"right\" && nextColumn?.getIsFirstColumn(\"right\");\n\n const showEndBorder = !isBeforeRightPinned && (isLastColumn || !isLastRightPinnedColumn);\n\n const showStartBorder = isFirstRightPinnedColumn;\n\n return {\n showEndBorder,\n showStartBorder\n };\n}\n\nexport function getColumnPinningStyle<TData>(params: {\n column: Column<TData>;\n withBorder?: boolean;\n dir?: Direction;\n}): React.CSSProperties {\n const { column, dir = \"ltr\", withBorder = false } = params;\n\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn = isPinned === \"left\" && column.getIsLastColumn(\"left\");\n const isFirstRightPinnedColumn = isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n const isRtl = dir === \"rtl\";\n\n const leftPosition = isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined;\n const rightPosition = isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined;\n\n /*\n * ⚠ Token của gói này là MÀU ĐẦY ĐỦ (`--card: #ffffff`), KHÔNG phải bộ 3 kênh HSL — nên viết\n * `var(--card)` trần, đừng bọc `hsl()`. Bản gốc port từ một hệ token kiểu `\"150 20% 98%\"` nên\n * ở đó `hsl(var(--card))` mới đúng; bê nguyên sang đây thì `hsl(#ffffff)` là giá trị KHÔNG\n * HỢP LỆ, và hệ quả là cột ghim trong suốt — nội dung cuộn phía dưới lộ xuyên qua. Không có\n * lỗi nào được ném ra: CSS bỏ qua khai báo hỏng rồi rơi về `background: transparent`.\n *\n * Nền lấy theo `--card` vì hộp lưới dùng `bg-card` như mọi bảng khác của gói.\n *\n * ⚠ Khác bản gốc (3.3.2): KHÔNG có `opacity`. Bản gốc đặt `opacity: 0.97` cho ô ghim — nền đã\n * đục nhưng cả Ô mờ 3%, nên khi cuộn ngang, chữ của cột đang trôi phía dưới lộ thành vệt mờ sau\n * cột ghim. Cùng một kết cục với lỗi `hsl()` ngay trên (cột ghim để lộ nội dung cuộn qua), chỉ\n * nhạt hơn — và cũng không cổng nào thấy, vì `opacity` nằm ở `style` inline. `test:grid-render`\n * nay canh cả chuyện này. Ranh giới vùng ghim ⇄ vùng cuộn đã có viền cuối cột lo.\n */\n return {\n boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? isRtl\n ? \"4px 0 4px -4px var(--border) inset\"\n : \"-4px 0 4px -4px var(--border) inset\"\n : isFirstRightPinnedColumn\n ? isRtl\n ? \"-4px 0 4px -4px var(--border) inset\"\n : \"4px 0 4px -4px var(--border) inset\"\n : undefined\n : undefined,\n left: isRtl ? rightPosition : leftPosition,\n right: isRtl ? leftPosition : rightPosition,\n position: isPinned ? \"sticky\" : \"relative\",\n background: \"var(--card)\",\n width: column.getSize(),\n zIndex: isPinned ? 1 : undefined\n };\n}\n\nexport function getScrollDirection(direction: string): \"left\" | \"right\" | \"home\" | \"end\" | undefined {\n if (direction === \"left\" || direction === \"right\" || direction === \"home\" || direction === \"end\") {\n return direction as \"left\" | \"right\" | \"home\" | \"end\";\n }\n if (direction === \"pageleft\") return \"left\";\n if (direction === \"pageright\") return \"right\";\n return undefined;\n}\n\nexport function scrollCellIntoView<TData>(params: {\n container: HTMLDivElement;\n targetCell: HTMLDivElement;\n tableRef: React.RefObject<Table<TData> | null>;\n viewportOffset: number;\n direction?: \"left\" | \"right\" | \"home\" | \"end\";\n isRtl: boolean;\n}): void {\n const { container, targetCell, tableRef, direction, viewportOffset, isRtl } = params;\n\n const containerRect = container.getBoundingClientRect();\n const cellRect = targetCell.getBoundingClientRect();\n\n const hasNegativeScroll = container.scrollLeft < 0;\n const isActuallyRtl = isRtl || hasNegativeScroll;\n\n const currentTable = tableRef.current;\n const leftPinnedColumns = currentTable?.getLeftVisibleLeafColumns() ?? [];\n const rightPinnedColumns = currentTable?.getRightVisibleLeafColumns() ?? [];\n\n const leftPinnedWidth = leftPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n const rightPinnedWidth = rightPinnedColumns.reduce((sum, c) => sum + c.getSize(), 0);\n\n const viewportLeft = isActuallyRtl\n ? containerRect.left + rightPinnedWidth + viewportOffset\n : containerRect.left + leftPinnedWidth + viewportOffset;\n const viewportRight = isActuallyRtl\n ? containerRect.right - leftPinnedWidth - viewportOffset\n : containerRect.right - rightPinnedWidth - viewportOffset;\n\n const isFullyVisible = cellRect.left >= viewportLeft && cellRect.right <= viewportRight;\n\n if (isFullyVisible) return;\n\n const isClippedLeft = cellRect.left < viewportLeft;\n const isClippedRight = cellRect.right > viewportRight;\n\n let scrollDelta = 0;\n\n if (!direction) {\n if (isClippedRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else if (isClippedLeft) {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n } else {\n const shouldScrollRight = isActuallyRtl\n ? direction === \"right\" || direction === \"home\"\n : direction === \"right\" || direction === \"end\";\n\n if (shouldScrollRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n }\n\n container.scrollLeft += scrollDelta;\n}\n\nfunction countTabs(s: string): number {\n let n = 0;\n for (let i = 0; i < s.length; i++) if (s[i] === \"\\t\") n++;\n return n;\n}\n\nexport function parseTsv(text: string, fallbackColumnCount: number): string[][] {\n if (text.startsWith('\"') || text.includes('\\t\"')) {\n const rows: string[][] = [];\n let currentRow: string[] = [];\n let currentField = \"\";\n let inQuotes = false;\n let i = 0;\n\n while (i < text.length) {\n const char = text[i];\n const nextChar = text[i + 1];\n\n if (inQuotes) {\n if (char === '\"' && nextChar === '\"') {\n currentField += '\"';\n i += 2;\n } else if (char === '\"') {\n inQuotes = false;\n i++;\n } else {\n currentField += char;\n i++;\n }\n } else {\n if (char === '\"' && currentField === \"\") {\n inQuotes = true;\n i++;\n } else if (char === \"\\t\") {\n currentRow.push(currentField);\n currentField = \"\";\n i++;\n } else if (char === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i++;\n } else if (char === \"\\r\" && nextChar === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i += 2;\n } else {\n currentField += char;\n i++;\n }\n }\n }\n\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n\n return rows;\n }\n\n const lines = text.split(\"\\n\").map((l) => l.replace(/\\r$/, \"\"));\n let maxTabCount = 0;\n for (const line of lines) {\n const n = countTabs(line);\n if (n > maxTabCount) maxTabCount = n;\n }\n const columnCount = maxTabCount > 0 ? maxTabCount + 1 : fallbackColumnCount;\n if (columnCount <= 0) return [];\n\n const expectedTabCount = columnCount - 1;\n const rows: string[][] = [];\n let buf = \"\";\n let bufTabCount = 0;\n\n for (const line of lines) {\n const tc = countTabs(line);\n\n if (tc === expectedTabCount) {\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n rows.push(line.split(\"\\t\"));\n } else {\n buf = buf ? `${buf}\\n${line}` : line;\n bufTabCount += tc;\n if (bufTabCount === expectedTabCount) {\n rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n }\n }\n }\n\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n\n return rows.length > 0 ? rows : lines.filter((l) => l.length > 0).map((l) => l.split(\"\\t\"));\n}\n\n/**\n * Phần tử có đang nằm trong một lớp nổi không? Dùng để lưới KHÔNG coi thao tác\n * trong popover/menu/hộp thoại là \"bấm ra ngoài\" (sẽ mất focus ô đang sửa).\n *\n * Primitive của package không gắn `data-slot`, nên bắt theo wrapper của Radix Popper\n * (bao Popover/DropdownMenu/Select/Tooltip) và theo role — kể cả Select \"item-aligned\".\n *\n * ⚠ Thêm ở `3.1.0`: `ActionBar` của gói (`data-slot=\"action-bar\"`). Thanh thao tác hàng loạt là bạn\n * đi kèm của lưới — nó mở khi có dòng được tick — nhưng nó là `role=\"toolbar\"` nằm NGOÀI lưới,\n * nên trước đây một cú bấm vào nó bị tính là \"bấm ra ngoài\": `mousedown` xoá lựa chọn dòng, thanh\n * đóng lại TRƯỚC khi `click` tới nơi, và hành động không bao giờ chạy. Đo trên comitor-ds: tick hai\n * dòng, bấm \"Mark as done\" → thanh biến mất, không dòng nào đổi. Cùng lý do, focus bàn phím đi\n * vào thanh cũng bị lưới kéo ngược về ô.\n */\nexport function getIsInPopover(element: unknown): boolean {\n if (!(element instanceof Element)) return false;\n\n return (\n element.closest(\"[data-grid-cell-editor]\") !== null ||\n element.closest(\"[data-grid-popover]\") !== null ||\n element.closest(\"[data-radix-popper-content-wrapper]\") !== null ||\n element.closest(\"[data-slot='action-bar']\") !== null ||\n element.closest(\"[role='dialog']\") !== null ||\n // `alertdialog` (3.1.0): `ConfirmDialog` của gói là `role=\"alertdialog\"` — thiếu dòng này thì\n // bấm Huỷ trong hộp xác nhận xoá các dòng đang tick, dù hộp mở ra chính vì các dòng đó.\n element.closest(\"[role='alertdialog']\") !== null ||\n element.closest(\"[role='menu']\") !== null ||\n element.closest(\"[role='listbox']\") !== null\n );\n}\n\n/**\n * Tên hiển thị của một cột: `meta.label` → `header` nếu là chuỗi → `id`. MỘT bản cho cả tiêu đề cột\n * lẫn tên truy cập của ô rỗng (`DataGridCellWrapper`), để hai chỗ không gọi cùng một cột bằng hai tên.\n * Không xuất qua barrel — nội bộ của lưới.\n */\nexport function getColumnLabel<TData, TValue>(column: Column<TData, TValue>): string {\n const { meta, header } = column.columnDef;\n if (meta?.label) return meta.label;\n return typeof header === \"string\" ? header : column.id;\n}\n\n/**\n * Ô \"rỗng\" theo nghĩa TRỢ NĂNG: không có gì để trình đọc màn hình đọc ra — `null`/`undefined`, chuỗi\n * chỉ có khoảng trắng, mảng rỗng (multi-select, file). `false` (checkbox) và `0` (number) KHÔNG rỗng:\n * ô vẫn vẽ ra một thứ có tên (checkbox có `aria-label`, số `0` là chữ).\n */\nexport function isEmptyCellValue(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value === \"string\") return value.trim() === \"\";\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\n/**\n * Icon + tên kiểu dữ liệu của một cột (hiện trong tooltip ở tiêu đề cột).\n *\n * `messages` bỏ trống = bảng mặc định (tiếng Anh), giữ đúng chữ ký của `1.11.0`. Component nào\n * vẽ nhãn này cho người dùng thì PHẢI truyền bảng đang có hiệu lực vào — quên là tooltip nói\n * tiếng Anh giữa một lưới tiếng Việt.\n */\nexport function getColumnVariant(\n variant?: CellOpts[\"variant\"],\n messages?: DataGridMessages\n): {\n icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;\n label: string;\n} | null {\n const msg = messages ?? getDataGridMessages();\n switch (variant) {\n case \"short-text\":\n return { label: msg.column.variant.shortText, icon: BaselineIcon };\n case \"long-text\":\n return { label: msg.column.variant.longText, icon: WrapTextIcon };\n case \"number\":\n return { label: msg.column.variant.number, icon: HashIcon };\n case \"url\":\n return { label: msg.column.variant.url, icon: LinkIcon };\n case \"checkbox\":\n return { label: msg.column.variant.checkbox, icon: CheckSquareIcon };\n case \"select\":\n return { label: msg.column.variant.select, icon: ListIcon };\n case \"multi-select\":\n return { label: msg.column.variant.multiSelect, icon: ListChecksIcon };\n case \"date\":\n return { label: msg.column.variant.date, icon: CalendarIcon };\n case \"file\":\n return { label: msg.column.variant.file, icon: FileIcon };\n default:\n return null;\n }\n}\n\nexport function getEmptyCellValue(variant: CellOpts[\"variant\"] | undefined): unknown {\n if (variant === \"multi-select\" || variant === \"file\") return [];\n if (variant === \"number\" || variant === \"date\" || variant === \"select\") return null;\n if (variant === \"checkbox\") return false;\n return \"\";\n}\n\nexport function getUrlHref(urlString: string): string {\n if (!urlString || urlString.trim() === \"\") return \"\";\n\n const trimmed = urlString.trim();\n\n // Reject dangerous protocols (extra safety, though our http:// prefix would neutralize them)\n if (/^(javascript|data|vbscript|file):/i.test(trimmed)) {\n return \"\";\n }\n\n if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n return trimmed;\n }\n\n return `http://${trimmed}`;\n}\n\nexport function parseLocalDate(dateStr: unknown): Date | null {\n if (!dateStr) return null;\n if (dateStr instanceof Date) return dateStr;\n if (typeof dateStr !== \"string\") return null;\n const [year, month, day] = dateStr.split(\"-\").map(Number);\n if (!year || !month || !day) return null;\n const date = new Date(year, month - 1, day);\n // Verify date wasn't auto-corrected (e.g. Feb 30 -> Mar 1)\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\nexport function formatDateToString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Mẫu định dạng ngày của lưới: ngày NGẮN theo locale date-fns — `en-US` ra `12/25/2024`,\n * `vi` / `en-GB` ra `25/12/2024`. Một mẫu cho cả hiển thị lẫn phép đọc lại ({@link parseDateInput}),\n * để thứ người dùng THẤY cũng là thứ dán vào được.\n */\nconst DATE_DISPLAY_FORMAT = \"P\";\n\n/**\n * Hiển thị ngày dạng ngắn theo `locale` (dùng cho ô ngày, bộ lọc ngày).\n *\n * Hàm THUẦN nên không đọc được ngôn ngữ của lưới: component truyền locale đang có hiệu lực vào\n * (`useDataGridDateLocale()`). Bỏ trống `locale` = locale mặc định của date-fns (`enUS`, trừ khi\n * app đã đổi bằng `setDefaultOptions`).\n *\n * ⚠ Trước `3.0.0` hàm này luôn ra `dd/MM/yyyy` bất kể ngôn ngữ. Cần lại đúng chuỗi cũ thì truyền\n * `vi` (`import { vi } from \"date-fns/locale\"`).\n */\nexport function formatDate(date: Date | string | number | null | undefined, locale?: DateFnsLocale): string {\n if (!date) return \"\";\n const value = date instanceof Date ? date : new Date(date);\n if (Number.isNaN(value.getTime())) return \"\";\n return format(value, DATE_DISPLAY_FORMAT, { locale });\n}\n\n/** Như {@link formatDate}, cho giá trị thô của ô ngày (`yyyy-MM-dd` hoặc `Date`). */\nexport function formatDateForDisplay(dateStr: unknown, locale?: DateFnsLocale): string {\n if (!dateStr) return \"\";\n const date = parseLocalDate(dateStr);\n if (!date) return typeof dateStr === \"string\" ? dateStr : \"\";\n return formatDate(date, locale);\n}\n\n/**\n * Đọc chuỗi ngày DÁN vào ô ngày. Thử dạng ngắn của `locale` trước — đúng dạng lưới đang hiển\n * thị, nên `25/12/2024` dán vào lưới tiếng Việt là 25 tháng 12, không phải lỗi — rồi mới tới\n * `new Date()` (ISO `yyyy-MM-dd`, và những gì JS tự hiểu). `null` khi không đọc được.\n *\n * Năm phải đủ 4 chữ số: mẫu `\"P\"` đọc `25/12/24` thành năm 24 sau Công nguyên, một ngày \"hợp lệ\"\n * mà không ai muốn — chuỗi như thế rơi xuống `new Date()` như trước.\n */\nexport function parseDateInput(text: string, locale?: DateFnsLocale): Date | null {\n const trimmed = text.trim();\n if (!trimmed) return null;\n const localized = parse(trimmed, DATE_DISPLAY_FORMAT, new Date(), { locale });\n if (isValid(localized) && localized.getFullYear() >= 1000) return localized;\n const date = new Date(trimmed);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Id cho tệp đính kèm. `crypto.randomUUID` chỉ có ở secure context (https/localhost) —\n * app nội bộ chạy http trong mạng bệnh viện sẽ không có, nên phải có đường lui.\n */\nexport function generateFileId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `file-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Chép text vào clipboard. `navigator.clipboard` cũng chỉ có ở secure context,\n * nên rơi về `document.execCommand(\"copy\")` (đã deprecated nhưng vẫn chạy mọi trình duyệt).\n * Ném lỗi khi cả hai đường đều không dùng được để phía gọi báo toast.\n */\nexport async function writeClipboardText(text: string): Promise<void> {\n if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n try {\n await navigator.clipboard.writeText(text);\n return;\n } catch {\n // Bị chặn quyền → thử đường lui bên dưới\n }\n }\n\n if (!copyWithExecCommand(text)) {\n throw new Error(\"Clipboard không khả dụng\");\n }\n}\n\nfunction copyWithExecCommand(text: string): boolean {\n if (typeof document === \"undefined\") return false;\n\n const textarea = document.createElement(\"textarea\");\n textarea.value = text;\n textarea.setAttribute(\"readonly\", \"\");\n textarea.style.position = \"fixed\";\n textarea.style.top = \"0\";\n textarea.style.insetInlineStart = \"-9999px\";\n document.body.appendChild(textarea);\n\n const previouslyFocused = document.activeElement;\n textarea.select();\n\n let copied = false;\n try {\n copied = document.execCommand(\"copy\");\n } catch {\n copied = false;\n }\n\n document.body.removeChild(textarea);\n if (previouslyFocused instanceof HTMLElement) {\n previouslyFocused.focus();\n }\n\n return copied;\n}\n\n/** Đọc clipboard khi có quyền; trả `null` nếu trình duyệt/ngữ cảnh không cho đọc. */\nexport async function readClipboardText(): Promise<string | null> {\n if (typeof navigator === \"undefined\" || !navigator.clipboard?.readText) return null;\n try {\n return await navigator.clipboard.readText();\n } catch {\n return null;\n }\n}\n\nexport function formatFileSize(bytes: number): string {\n if (bytes <= 0 || !Number.isFinite(bytes)) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(k)));\n return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n}\n\nexport function getFileIcon(type: string): React.ComponentType<React.SVGProps<SVGSVGElement>> {\n if (type.startsWith(\"image/\")) return FileImage;\n if (type.startsWith(\"video/\")) return FileVideo;\n if (type.startsWith(\"audio/\")) return FileAudio;\n if (type.includes(\"pdf\")) return FileText;\n if (type.includes(\"zip\") || type.includes(\"rar\")) return FileArchive;\n if (type.includes(\"word\") || type.includes(\"document\") || type.includes(\"doc\")) return FileText;\n if (type.includes(\"sheet\") || type.includes(\"excel\") || type.includes(\"xls\")) return FileSpreadsheet;\n if (type.includes(\"presentation\") || type.includes(\"powerpoint\") || type.includes(\"ppt\")) return Presentation;\n return File;\n}\n"],"mappings":"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","names":["rows"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Editor } from '@tiptap/core';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { g as EditorFormat } from '../editor-types-BacXD2VN.js';
|
|
4
4
|
import { MarkdownEditorLabels } from './markdown-editor-constants.js';
|
|
5
5
|
import '../components/composite/empty-state.js';
|
|
6
6
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { f as EditorBaseProps, g as EditorFormat, a as EditorToolbarMode, E as EditorHandleBase } from '../editor-types-BacXD2VN.js';
|
|
3
3
|
import '@tiptap/core';
|
|
4
4
|
import '../components/composite/empty-state.js';
|
|
5
5
|
import './markdown-editor-constants.js';
|
|
@@ -24,6 +24,6 @@ interface EditorCoreProps<THandle> extends EditorBaseProps {
|
|
|
24
24
|
*/
|
|
25
25
|
createHandle: (core: EditorCoreHandle) => THandle;
|
|
26
26
|
}
|
|
27
|
-
declare function EditorCore<THandle>({ format, toolbar, value, defaultValue, onChange, onSubmit, onFocus, onBlur, placeholder, editable, autoFocus, variant, mentions, onImageUpload, onImageUploadError, resolveImageSrc, extensions: extraExtensions, labels: labelsProp, name, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, className, contentClassName, slot, handleRef, createHandle }: EditorCoreProps<THandle>): React.JSX.Element;
|
|
27
|
+
declare function EditorCore<THandle>({ format, toolbar, value, defaultValue, onChange, onSubmit, submitOnEnter, onFocus, onBlur, placeholder, editable, autoFocus, variant, mentions, onImageUpload, onImageUploadError, resolveImageSrc, extensions: extraExtensions, labels: labelsProp, name, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, className, contentClassName, slot, handleRef, createHandle }: EditorCoreProps<THandle>): React.JSX.Element;
|
|
28
28
|
|
|
29
29
|
export { EditorCore, type EditorCoreHandle, type EditorCoreProps };
|
|
@@ -51,6 +51,7 @@ function EditorCore({
|
|
|
51
51
|
defaultValue,
|
|
52
52
|
onChange,
|
|
53
53
|
onSubmit,
|
|
54
|
+
submitOnEnter = false,
|
|
54
55
|
onFocus,
|
|
55
56
|
onBlur,
|
|
56
57
|
placeholder,
|
|
@@ -107,6 +108,11 @@ function EditorCore({
|
|
|
107
108
|
if (!onSubmit) return false;
|
|
108
109
|
onSubmit();
|
|
109
110
|
return true;
|
|
111
|
+
},
|
|
112
|
+
onEnterShortcut: () => {
|
|
113
|
+
if (!submitOnEnter || !onSubmit || slashStore.getSnapshot() || mentionStore.getSnapshot()) return false;
|
|
114
|
+
onSubmit();
|
|
115
|
+
return true;
|
|
110
116
|
}
|
|
111
117
|
});
|
|
112
118
|
const initialValue = value ?? defaultValue ?? "";
|
|
@@ -1 +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; kind: string | null };\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 resolveMentionHref: mentions?.getHref\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 kind: item.kind ?? null\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 // Id của hai loại có thể trùng nhau (người `u1` và việc `u1`) — khoá phải mang cả loại.\n getKey={(item) => `${item.kind ?? \"\"}:${item.id}`}\n exitWhenEmpty={(query) => /\\s/.test(query)}\n renderItem={(item) => (\n <>\n {item.icon ? (\n <span aria-hidden=\"true\" className=\"flex size-6 shrink-0 items-center justify-center\">\n <item.icon className=\"size-4 text-muted-foreground\" />\n </span>\n ) : (\n <LetterAvatar name={item.label} src={item.avatarUrl} size=\"xs\" aria-hidden=\"true\" />\n )}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.description && (\n <span className=\"max-w-[45%] truncate text-xs text-muted-foreground\">{item.description}</span>\n )}\n </>\n )}\n />\n )}\n\n {onImageUpload && (\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n hidden\n tabIndex={-1}\n onChange={(event) => {\n const files = Array.from(event.target.files ?? []).filter((file) => file.type.startsWith(\"image/\"));\n // Xoá giá trị để chọn LẠI đúng tệp đó lần sau vẫn phát `change`.\n event.target.value = \"\";\n if (editor && files.length > 0)\n insertImageFiles(editor, files, undefined, imageHandlers.current, pendingUploads);\n }}\n />\n )}\n\n {name && <input type=\"hidden\" name={name} value={hiddenValue} />}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/editor/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; kind: string | null };\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 submitOnEnter = false,\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 resolveMentionHref: mentions?.getHref\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 onEnterShortcut: () => {\n // Menu `@`/`/` đang mở: Enter là CHỌN mục — plugin gợi ý đứng sau keymap này (ưu tiên 1000).\n if (!submitOnEnter || !onSubmit || slashStore.getSnapshot() || mentionStore.getSnapshot()) 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 kind: item.kind ?? null\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 // Id của hai loại có thể trùng nhau (người `u1` và việc `u1`) — khoá phải mang cả loại.\n getKey={(item) => `${item.kind ?? \"\"}:${item.id}`}\n exitWhenEmpty={(query) => /\\s/.test(query)}\n renderItem={(item) => (\n <>\n {item.icon ? (\n <span aria-hidden=\"true\" className=\"flex size-6 shrink-0 items-center justify-center\">\n <item.icon className=\"size-4 text-muted-foreground\" />\n </span>\n ) : (\n <LetterAvatar name={item.label} src={item.avatarUrl} size=\"xs\" aria-hidden=\"true\" />\n )}\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":";AAgZQ,SAqCI,UArCJ,KAqCI,YArCJ;AAhYR,SAAS,iBAAiB;AAC1B,SAAS,eAAe,iBAAiB;AACzC,YAAY,WAAW;AACvB,SAAS,oBAAoB;AAC7B,SAAS,aAAa;AACtB,SAAS,wBAAwB;AACjC,SAAS,UAAU;AACnB,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAQ9B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EAEA;AAAA,OAEK;AACP,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AA0BhC,MAAM,qBAAqB,IAAI,UAAU,oBAAoB;AAG7D,MAAM,qBAAqB;AAE3B,SAAS,mBAAmB,OAA6C,OAA4C;AACnH,QAAM,OAAO,MAAM,KAAK;AACxB,QAAM,UAAU,OACZ,MAAM;AAAA,IACJ,CAAC,SACC,cAAc,KAAK,OAAO,IAAI;AAAA,IAE9B,cAAc,KAAK,MAAM,QAAQ,QAAQ,EAAE,GAAG,KAAK,QAAQ,QAAQ,EAAE,CAAC,MACrE,KAAK,cAAc,cAAc,KAAK,aAAa,IAAI,IAAI;AAAA,EAChE,IACA;AACJ,SAAO,QAAQ,MAAM,GAAG,kBAAkB;AAC5C;AAOA,SAAS,UAAU,QAAgB,QAA8B;AAC/D,MAAI,WAAW,WAAY,QAAO,aAAa,OAAO,YAAY,CAAC;AACnE,SAAO,OAAO,UAAU,KAAK,OAAO,QAAQ;AAC9C;AAEA,MAAM,YAAY,CAAC,UAAkD,UAAU,QAAQ,UAAU;AAE1F,SAAS,WAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,mBAAmB;AAAA,EACnB,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,SAAS,iBAAiB,kBAAkB,UAAU;AAO5D,QAAM,UAAU,SAAgC;AAAA,IAC9C,aAAa,eAAe,OAAO;AAAA,IACnC,cAAc,OAAO;AAAA,IACrB;AAAA,IACA,qBAAqB,UAAU;AAAA,IAC/B,oBAAoB,UAAU;AAAA,EAChC,CAAC;AACD,QAAM,SAAS,SAAS,EAAE,UAAU,SAAS,QAAQ,UAAU,eAAe,QAAQ,KAAK,CAAC;AAC5F,QAAM,gBAAgB,SAA8B;AAAA,IAClD,QAAQ;AAAA,IACR,SAAS,CAAC,OAAO,SAAS;AACxB,UAAI,mBAAoB,oBAAmB,OAAO,IAAI;AAAA,UACjD,OAAM,OAAO,mBAAmB,EAAE,SAAS,eAAe,aAAa,KAAK,QAAQ,OAAU,CAAC;AAAA,IACtG;AAAA,IACA,YAAY;AAAA,IACZ,gBAAgB,OAAO;AAAA,IACvB;AAAA,EACF,CAAC;AAED,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,CAAC;AACtD,QAAM,cAAc,MAAM,YAAY,MAAM,eAAe,CAAC,UAAU,QAAQ,CAAC,GAAG,CAAC,CAAC;AACpF,QAAM,iBAAiB,SAA+B;AAAA,IACpD,gBAAgB,MAAM;AACpB,UAAI,CAAC,SAAU,QAAO;AACtB,kBAAY;AACZ,aAAO;AAAA,IACT;AAAA,IACA,kBAAkB,MAAM;AACtB,UAAI,CAAC,SAAU,QAAO;AACtB,eAAS;AACT,aAAO;AAAA,IACT;AAAA,IACA,iBAAiB,MAAM;AAErB,UAAI,CAAC,iBAAiB,CAAC,YAAY,WAAW,YAAY,KAAK,aAAa,YAAY,EAAG,QAAO;AAClG,eAAS;AACT,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AAED,QAAM,eAAe,SAAS,gBAAgB;AAC9C,QAAM,YAAY,MAAM,OAAO,YAAY;AAC3C,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,YAAY;AACjE,QAAM,iBAAiB,WAA2B,MAAM,oBAAI,IAAI,CAAC,EAAE;AACnE,QAAM,eAAe,MAAM,OAAyB,IAAI;AACxD,QAAM,YAAY,MAAM,YAAY,MAAM,aAAa,SAAS,MAAM,GAAG,CAAC,CAAC;AAE3E,QAAM,aAAa;AAAA,IACjB,MAAM,IAAI,gBAA8C,kBAAkB,CAAC,SAAS,IAAI;AAAA,EAC1F,EAAE;AACF,QAAM,eAAe;AAAA,IACnB,MACE,IAAI,gBAA6D,oBAAoB,CAAC,UAAU;AAAA,MAC9F,IAAI,KAAK;AAAA,MACT,OAAO,KAAK;AAAA,MACZ,MAAM,KAAK,QAAQ;AAAA,IACrB,EAAE;AAAA,EACN,EAAE;AAEF,QAAM,CAAC,UAAU,IAAI,MAAM,SAAyB,MAAM;AAAA,IACxD,GAAG,uBAAuB;AAAA,MACxB;AAAA,MACA;AAAA,MACA,mBAAmB;AAAA,QACjB,WAAW;AAAA,QACX,MAAM;AAAA;AAAA;AAAA,QAGN,aAAa;AAAA,QACb,UAAU,OAAO,UAAU,UAAU,aAAa,MAAM;AAAA,QACxD,OAAO,OAAO,EAAE,OAAO,OAAO,MAAM;AAClC,gBAAM,SAAS,OAAO,QAAQ,UAAU;AACxC,cAAI,CAAC,OAAQ,QAAO,CAAC;AACrB,cAAI,OAAO,WAAW,WAAY,QAAO,CAAC,GAAI,MAAM,OAAO,OAAO,EAAE,OAAO,CAAC,CAAE;AAC9E,iBAAO,mBAAmB,QAAQ,KAAK;AAAA,QACzC;AAAA,QACA,OAAO,CAAC,EAAE,QAAQ,QAAQ,MACxB,QAAQ,OAAO,QAAQ,QAAQ,KAC/B,QAAQ,cACR,CAAC,QAAQ,SAAS,WAAW,KAC7B,CAAC,QAAQ,SAAS,MAAM;AAAA,QAC1B,QAAQ,aAAa;AAAA,MACvB;AAAA,IACF,CAAC;AAAA,IACD,4BAA4B;AAAA,MAC1B,OAAO,CAAC,EAAE,QAAQ,SAAS,MAAM,MAC/B,oBAAoB,SAAS,OAAO,OAAO,QAAQ,QAAQ;AAAA,QACzD;AAAA,QACA,WAAW,OAAO,QAAQ,gBAAgB,MAAM,aAAa,SAAS,MAAM,IAAI;AAAA,MAClF,CAAC;AAAA,MACH,QAAQ,WAAW;AAAA,IACrB,CAAC;AAAA,IACD,2BAA2B,eAAe,cAAc;AAAA,IACxD,mBAAmB,gBAAgB,MAAM;AAAA,IACzC,GAAI,mBAAmB,CAAC;AAAA,EAC1B,CAAC;AASD,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,MAAM,SAAwB,IAAI;AAE9E,QAAM,iBAAiB,YAAY,YAAY,uBAAuB;AAatE,QAAM,cAAc,MAAM,QAAQ,MAAM;AACtC,UAAM,aAAqC;AAAA,MACzC,OAAO,GAAG,+BAA+B,gBAAgB,gBAAgB;AAAA,IAC3E;AACA,QAAI,GAAI,YAAW,KAAK;AACxB,QAAI,CAAC,SAAU,YAAW,OAAO;AACjC,QAAI,UAAU;AACZ,iBAAW,OAAO;AAClB,iBAAW,gBAAgB,IAAI;AAC/B,YAAM,aAAa,kBAAkB;AACrC,UAAI,aAAa,CAAC,WAAY,YAAW,YAAY,IAAI,aAAa,OAAO;AAC7E,UAAI,WAAY,YAAW,iBAAiB,IAAI;AAChD,UAAI,UAAU,YAAY,EAAG,YAAW,eAAe,IAAI;AAAA,IAC7D;AACA,QAAI,gBAAiB,YAAW,kBAAkB,IAAI;AACtD,QAAI,UAAU,WAAW,EAAG,YAAW,cAAc,IAAI;AACzD,WAAO,EAAE,WAAW;AAAA,EACtB,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,EACT,CAAC;AAED,QAAM,SAAS,UAAU;AAAA,IACvB;AAAA,IACA,SAAS;AAAA,IACT,aAAa;AAAA,IACb;AAAA,IACA,WAAW,YAAY,QAAQ;AAAA;AAAA;AAAA,IAG/B,mBAAmB;AAAA,IACnB;AAAA,IACA,UAAU,CAAC,EAAE,QAAQ,QAAQ,MAAM;AACjC,YAAM,OAAO,UAAU,SAAS,MAAM;AACtC,gBAAU,UAAU;AACpB,aAAO,QAAQ,WAAW,IAAI;AAC9B,UAAI,OAAO,QAAQ,KAAM,gBAAe,IAAI;AAAA,IAC9C;AAAA,IACA,SAAS,MAAM,OAAO,QAAQ,UAAU;AAAA,IACxC,QAAQ,MAAM,OAAO,QAAQ,SAAS;AAAA,EACxC,CAAC;AAID,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,UAAU,UAAU,UAAa,UAAU,UAAU,QAAS;AACnE,WAAO,SAAS,WAAW,OAAO,EAAE,aAAa,QAAQ,YAAY,MAAM,CAAC;AAC5E,cAAU,UAAU;AACpB,mBAAe,KAAK;AAAA,EACtB,GAAG,CAAC,QAAQ,OAAO,MAAM,CAAC;AAI1B,QAAM,UAAU,MAAM;AACpB,QAAI,UAAU,OAAO,eAAe,SAAU,QAAO,YAAY,UAAU,KAAK;AAAA,EAClF,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAGrB,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,UAAU,CAAC,MAAM,CAAC,YAAY,aAAa,eAAgB;AAChE,UAAM,QAAQ,SAAS,cAAgC,cAAc,IAAI,OAAO,EAAE,CAAC,IAAI;AACvF,QAAI,CAAC,MAAO;AACZ,QAAI,CAAC,MAAM,GAAI,OAAM,KAAK,GAAG,EAAE;AAC/B,sBAAkB,MAAM,EAAE;AAC1B,UAAM,cAAc,CAAC,UAAsB;AACzC,YAAM,eAAe;AACrB,aAAO,SAAS,MAAM;AAAA,IACxB;AACA,UAAM,iBAAiB,SAAS,WAAW;AAC3C,WAAO,MAAM;AACX,YAAM,oBAAoB,SAAS,WAAW;AAC9C,wBAAkB,IAAI;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,QAAQ,IAAI,UAAU,WAAW,cAAc,CAAC;AAOpD,QAAM,YAAY,SAAS,MAAM;AACjC,QAAM,OAAO,MAAM;AAAA,IACjB,OAAO;AAAA,MACL,OAAO,CAAC,WAAW,UAAU;AAC3B,kBAAU,SAAS,SAAS,MAAM,QAAQ;AAAA,MAC5C;AAAA,MACA,UAAU,MAAO,UAAU,UAAU,UAAU,UAAU,SAAS,MAAM,IAAI,UAAU;AAAA,MACtF,UAAU,CAAC,SAAS;AAClB,kBAAU,SAAS,SAAS,WAAW,MAAM,EAAE,aAAa,QAAQ,YAAY,KAAK,CAAC;AAAA,MACxF;AAAA,MACA,OAAO,MAAM;AACX,kBAAU,SAAS,SAAS,aAAa,IAAI;AAAA,MAC/C;AAAA,MACA,SAAS,MAAO,UAAU,UAAU,UAAU,QAAQ,UAAU,UAAU,QAAQ,KAAK,MAAM;AAAA,MAC7F,aAAa,MAAM,eAAe,OAAO;AAAA,MACzC,IAAI,SAAS;AACX,eAAO,UAAU;AAAA,MACnB;AAAA,IACF;AAAA,IACA,CAAC,WAAW,gBAAgB,MAAM;AAAA,EACpC;AACA,QAAM,oBAAoB,WAAW,MAAM,aAAa,IAAI,GAAG,CAAC,cAAc,IAAI,CAAC;AAEnF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAW;AAAA,MACX,gBAAc;AAAA,MACd,gBAAc;AAAA,MACd,iBAAe,CAAC,YAAY;AAAA,MAC5B,WAAW;AAAA,QACT;AAAA,QACA,YAAY;AAAA;AAAA,QAGV;AAAA,QACF;AAAA,MACF;AAAA,MAEC;AAAA,oBAAY,WAAW,YACtB;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA,QAAQ;AAAA,YACR,aAAa,gBAAgB,YAAY;AAAA,YAGzC,WAAW,YAAY,YAAY,wBAAwB;AAAA;AAAA,QAC7D;AAAA,QAGD,SACC,oBAAC,iBAAc,QAAgB;AAAA;AAAA,UAG/B,oBAAC,SAAI,eAAY,QAAO,WAAW,GAAG,gBAAgB,gBAAgB,GAAG;AAAA;AAAA,QAG1E,UAAU,YACT;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA,YAAY,YAAY;AAAA,YACxB;AAAA;AAAA,QACF;AAAA,QAGD,UACC;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,OAAO;AAAA,YACP,OAAO,OAAO;AAAA,YACd,YAAY,OAAO;AAAA,YACnB,QAAQ,CAAC,SAAS,KAAK;AAAA,YACvB,YAAY,CAAC,SACX,iCACE;AAAA,kCAAC,KAAK,MAAL,EAAU,eAAY,QAAO;AAAA,cAC9B,oBAAC,UAAK,WAAU,2BAA2B,eAAK,OAAM;AAAA,cACrD,KAAK;AAAA,cAEJ,oBAAC,UAAK,WAAU,mDAAmD,eAAK,MAAK;AAAA,eAEjF;AAAA;AAAA,QAEJ;AAAA,QAGD,UAAU,YACT;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,OAAO;AAAA,YACP,OAAO,OAAO;AAAA,YACd,YAAY,OAAO;AAAA,YACnB,cAAc,OAAO;AAAA,YAErB,QAAQ,CAAC,SAAS,GAAG,KAAK,QAAQ,EAAE,IAAI,KAAK,EAAE;AAAA,YAC/C,eAAe,CAAC,UAAU,KAAK,KAAK,KAAK;AAAA,YACzC,YAAY,CAAC,SACX,iCACG;AAAA,mBAAK,OACJ,oBAAC,UAAK,eAAY,QAAO,WAAU,oDACjC,8BAAC,KAAK,MAAL,EAAU,WAAU,gCAA+B,GACtD,IAEA,oBAAC,gBAAa,MAAM,KAAK,OAAO,KAAK,KAAK,WAAW,MAAK,MAAK,eAAY,QAAO;AAAA,cAEpF,oBAAC,UAAK,WAAU,2BAA2B,eAAK,OAAM;AAAA,cACrD,KAAK,eACJ,oBAAC,UAAK,WAAU,sDAAsD,eAAK,aAAY;AAAA,eAE3F;AAAA;AAAA,QAEJ;AAAA,QAGD,iBACC;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,MAAK;AAAA,YACL,QAAO;AAAA,YACP,UAAQ;AAAA,YACR,QAAM;AAAA,YACN,UAAU;AAAA,YACV,UAAU,CAAC,UAAU;AACnB,oBAAM,QAAQ,MAAM,KAAK,MAAM,OAAO,SAAS,CAAC,CAAC,EAAE,OAAO,CAAC,SAAS,KAAK,KAAK,WAAW,QAAQ,CAAC;AAElG,oBAAM,OAAO,QAAQ;AACrB,kBAAI,UAAU,MAAM,SAAS;AAC3B,iCAAiB,QAAQ,OAAO,QAAW,cAAc,SAAS,cAAc;AAAA,YACpF;AAAA;AAAA,QACF;AAAA,QAGD,QAAQ,oBAAC,WAAM,MAAK,UAAS,MAAY,OAAO,aAAa;AAAA;AAAA;AAAA,EAChE;AAEJ;","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import '@tiptap/core';
|
|
2
2
|
import '../components/composite/empty-state.js';
|
|
3
|
-
export {
|
|
3
|
+
export { f as EditorBaseProps, g as EditorFormat, E as EditorHandleBase, a as EditorToolbarMode, b as MarkdownEditorMentionItem, c as MarkdownEditorMentionSource, d as MarkdownEditorMentions, e as MarkdownEditorVariant } from '../editor-types-BacXD2VN.js';
|
|
4
4
|
import './markdown-editor-constants.js';
|
|
5
5
|
import 'react';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { Editor } from '@tiptap/core';
|
|
3
|
-
import {
|
|
3
|
+
import { g as EditorFormat } from '../editor-types-BacXD2VN.js';
|
|
4
4
|
import { MarkdownEditorLabels } from './markdown-editor-constants.js';
|
|
5
5
|
import '../components/composite/empty-state.js';
|
|
6
6
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
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-
|
|
2
|
+
export { I as ImageUploadHandlers, M as MarkdownEditorImageUploadResult, P as PendingUploads, h as createImageUploadExtension, i as imageFilesFrom, j as insertImageFiles } from '../editor-types-BacXD2VN.js';
|
|
3
3
|
import '../components/composite/empty-state.js';
|
|
4
4
|
import 'react';
|
|
5
5
|
import './markdown-editor-constants.js';
|
package/dist/editor/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { E as EditorHandleBase, a as EditorToolbarMode, M as MarkdownEditorImageUploadResult, b as MarkdownEditorMentionItem, c as MarkdownEditorMentionSource, d as MarkdownEditorMentions, e as MarkdownEditorVariant } from '../editor-types-
|
|
1
|
+
export { E as EditorHandleBase, a as EditorToolbarMode, M as MarkdownEditorImageUploadResult, b as MarkdownEditorMentionItem, c as MarkdownEditorMentionSource, d as MarkdownEditorMentions, e as MarkdownEditorVariant } from '../editor-types-BacXD2VN.js';
|
|
2
2
|
export { MarkdownEditor, MarkdownEditorHandle, MarkdownEditorProps } from './markdown-editor.js';
|
|
3
3
|
export { DEFAULT_MARKDOWN_EDITOR_LABELS, MarkdownEditorBlockId, MarkdownEditorLabels } from './markdown-editor-constants.js';
|
|
4
4
|
export { RichTextEditor, RichTextEditorHandle, RichTextEditorProps } from './rich-text-editor.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { f as EditorBaseProps, a as EditorToolbarMode, E as EditorHandleBase } from '../editor-types-BacXD2VN.js';
|
|
3
3
|
import '@tiptap/core';
|
|
4
4
|
import '../components/composite/empty-state.js';
|
|
5
5
|
import './markdown-editor-constants.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Extension, AnyExtension } from '@tiptap/core';
|
|
2
2
|
import { MentionOptions } from '@tiptap/extension-mention';
|
|
3
3
|
import { Slice } from '@tiptap/pm/model';
|
|
4
|
-
import {
|
|
4
|
+
import { g as EditorFormat } from '../editor-types-BacXD2VN.js';
|
|
5
5
|
import '../components/composite/empty-state.js';
|
|
6
6
|
import 'react';
|
|
7
7
|
import './markdown-editor-constants.js';
|
|
@@ -84,6 +84,8 @@ interface EditorKeymapHandlers {
|
|
|
84
84
|
onLinkShortcut: () => boolean;
|
|
85
85
|
/** ⌘/Ctrl+Enter ngoài danh sách việc. Trả `false` để phím đi tiếp (xuống dòng cứng của Tiptap). */
|
|
86
86
|
onSubmitShortcut: () => boolean;
|
|
87
|
+
/** Enter trần (chỉ khi `submitOnEnter`). Trả `false` để phím đi tiếp (đoạn mới, mục danh sách mới…). */
|
|
88
|
+
onEnterShortcut: () => boolean;
|
|
87
89
|
}
|
|
88
90
|
/**
|
|
89
91
|
* Phím tắt của RIÊNG gói, ưu tiên cao hơn mặc định của Tiptap.
|
|
@@ -283,7 +283,9 @@ function createEditorKeymap(handlers, format = "markdown") {
|
|
|
283
283
|
if (toggled || handlers.current.onSubmitShortcut()) return true;
|
|
284
284
|
return swallowInCell(editor);
|
|
285
285
|
},
|
|
286
|
-
"Shift-Enter": ({ editor }) => swallowInCell(editor)
|
|
286
|
+
"Shift-Enter": ({ editor }) => swallowInCell(editor),
|
|
287
|
+
// `view.composing`: Enter đang chốt chữ của bộ gõ (Telex/VNI qua IME hệ thống) không được gửi tin.
|
|
288
|
+
Enter: ({ editor }) => !editor.view.composing && handlers.current.onEnterShortcut()
|
|
287
289
|
};
|
|
288
290
|
}
|
|
289
291
|
});
|