@thangph2146/nextjs-editor 1.0.2 → 1.0.4
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 +133 -1
- package/dist/index.cjs +44 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +39 -27
- package/dist/index.js.map +1 -1
- package/dist/uploads-hooks.cjs +89 -0
- package/dist/uploads-hooks.cjs.map +1 -0
- package/dist/uploads-hooks.d.cts +79 -0
- package/dist/uploads-hooks.d.ts +79 -0
- package/dist/uploads-hooks.js +59 -0
- package/dist/uploads-hooks.js.map +1 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -61,9 +61,141 @@ export function MyEditor() {
|
|
|
61
61
|
|
|
62
62
|
## Ghi chú
|
|
63
63
|
|
|
64
|
-
- **Ảnh**:
|
|
64
|
+
- **Ảnh**: Mặc định dialog chèn ảnh có tab **URL** và **File**. Để tab **Thư viện** hiển thị ảnh từ API của project, xem [Cấu hình API thư viện ảnh](#cấu-hình-api-thư-viện-ảnh) bên dưới.
|
|
65
65
|
- **Next.js**: Package dùng `next/image` và `next/dynamic` khi chạy trong Next.js; cần cài `next` trong project.
|
|
66
66
|
|
|
67
|
+
## Cấu hình API thư viện ảnh
|
|
68
|
+
|
|
69
|
+
Tab **Thư viện** trong dialog chèn ảnh cần dữ liệu từ API/uploads của project. Bạn cấu hình bằng cách **alias** module uploads-hooks của package sang file hooks trong project.
|
|
70
|
+
|
|
71
|
+
### Bước 1: Alias trong Next.js
|
|
72
|
+
|
|
73
|
+
Trong `next.config.ts` (hoặc `next.config.js`), thêm `webpack.resolve.alias` để trỏ `@thangph2146/nextjs-editor/uploads-hooks` sang file hooks uploads của project:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// next.config.ts
|
|
77
|
+
import type { NextConfig } from "next";
|
|
78
|
+
import path from "path";
|
|
79
|
+
|
|
80
|
+
const nextConfig: NextConfig = {
|
|
81
|
+
// ... config khác
|
|
82
|
+
webpack: (config) => {
|
|
83
|
+
config.resolve ??= {};
|
|
84
|
+
config.resolve.alias = {
|
|
85
|
+
...config.resolve.alias,
|
|
86
|
+
"@thangph2146/nextjs-editor/uploads-hooks": path.resolve(
|
|
87
|
+
__dirname,
|
|
88
|
+
"src/features/uploads/hooks/use-uploads-queries.ts" // đường dẫn tới file hooks của bạn
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
return config;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export default nextConfig;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Đường dẫn file có thể khác tùy cấu trúc project (ví dụ `src/lib/uploads-queries.ts`).
|
|
99
|
+
|
|
100
|
+
### Bước 2: File hooks phải export `useImagesList`
|
|
101
|
+
|
|
102
|
+
File được alias tới phải export hook `useImagesList(page, limit)` với đúng chuẩn:
|
|
103
|
+
|
|
104
|
+
- **Tham số:** `page: number`, `limit: number` (ví dụ `useImagesList(1, 100)`).
|
|
105
|
+
- **Giá trị trả về:** object tương thích với TanStack Query (useQuery), ít nhất:
|
|
106
|
+
- `data`: response từ API, có dạng:
|
|
107
|
+
- `data.data.folderTree`: cây thư mục ảnh (xem kiểu `FolderNode` bên dưới).
|
|
108
|
+
- `data.data.pagination`: `{ page, limit, total, totalPages }` (tùy chọn).
|
|
109
|
+
- `isLoading`: boolean.
|
|
110
|
+
|
|
111
|
+
**Kiểu dữ liệu (TypeScript):**
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
interface ImageItem {
|
|
115
|
+
fileName: string;
|
|
116
|
+
originalName: string;
|
|
117
|
+
size: number;
|
|
118
|
+
mimeType: string;
|
|
119
|
+
url: string; // URL đầy đủ để hiển thị ảnh (img src)
|
|
120
|
+
relativePath: string;
|
|
121
|
+
createdAt: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface FolderNode {
|
|
125
|
+
name: string;
|
|
126
|
+
path: string;
|
|
127
|
+
images: ImageItem[];
|
|
128
|
+
subfolders: FolderNode[]; // đệ quy
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Ví dụ hook (gọi API):**
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
// useImagesList gọi GET /api/uploads?page=1&limit=100 (hoặc endpoint của bạn)
|
|
136
|
+
export function useImagesList(page = 1, limit = 50) {
|
|
137
|
+
return useQuery({
|
|
138
|
+
queryKey: ["uploads", "images", page, limit],
|
|
139
|
+
queryFn: async () => {
|
|
140
|
+
const res = await fetch(`/api/uploads?page=${page}&limit=${limit}`);
|
|
141
|
+
const json = await res.json();
|
|
142
|
+
if (!json.success) throw new Error("Failed to fetch images");
|
|
143
|
+
return json; // { success, data: { data: [], folderTree, pagination } }
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Bước 3: API backend trả về `folderTree`
|
|
150
|
+
|
|
151
|
+
Endpoint list ảnh (ví dụ `GET /api/uploads` hoặc `GET /api/admin/uploads`) nên trả về JSON có **folderTree** — cây thư mục chứa ảnh, để editor hiển thị theo cấu trúc thư mục.
|
|
152
|
+
|
|
153
|
+
**Ví dụ response:**
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"success": true,
|
|
158
|
+
"data": {
|
|
159
|
+
"data": [],
|
|
160
|
+
"folderTree": {
|
|
161
|
+
"name": "images",
|
|
162
|
+
"path": "images",
|
|
163
|
+
"images": [
|
|
164
|
+
{
|
|
165
|
+
"fileName": "2024/01/15/abc.jpg",
|
|
166
|
+
"originalName": "abc.jpg",
|
|
167
|
+
"size": 12345,
|
|
168
|
+
"mimeType": "image/jpeg",
|
|
169
|
+
"url": "https://example.com/api/uploads/serve/images/2024/01/15/abc.jpg",
|
|
170
|
+
"relativePath": "images/2024/01/15/abc.jpg",
|
|
171
|
+
"createdAt": 1705312800000
|
|
172
|
+
}
|
|
173
|
+
],
|
|
174
|
+
"subfolders": [
|
|
175
|
+
{
|
|
176
|
+
"name": "2024",
|
|
177
|
+
"path": "images/2024",
|
|
178
|
+
"images": [],
|
|
179
|
+
"subfolders": []
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
"pagination": { "page": 1, "limit": 100, "total": 50, "totalPages": 1 }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- `folderTree.url` (hoặc mỗi `ImageItem.url`) phải là URL mà trình duyệt tải được (absolute hoặc same-origin).
|
|
189
|
+
- Nếu không có alias, tab Thư viện sẽ hiển thị "Chưa có hình ảnh nào được upload" (stub mặc định).
|
|
190
|
+
|
|
191
|
+
### Tóm tắt
|
|
192
|
+
|
|
193
|
+
| Việc | Mô tả |
|
|
194
|
+
|------|--------|
|
|
195
|
+
| Alias | `@thangph2146/nextjs-editor/uploads-hooks` → file hooks uploads của project (next.config webpack). |
|
|
196
|
+
| Hook | File đó export `useImagesList(page, limit)` trả về `{ data, isLoading }`, `data.data.folderTree` kiểu `FolderNode`. |
|
|
197
|
+
| API | Backend trả về list ảnh kèm `folderTree` (cây thư mục + ảnh trong từng thư mục). |
|
|
198
|
+
|
|
67
199
|
## Build từ source
|
|
68
200
|
|
|
69
201
|
```bash
|
package/dist/index.cjs
CHANGED
|
@@ -1166,32 +1166,6 @@ var init_tabs = __esm({
|
|
|
1166
1166
|
}
|
|
1167
1167
|
});
|
|
1168
1168
|
|
|
1169
|
-
// src/features/uploads/hooks/use-uploads-queries.ts
|
|
1170
|
-
var emptyFolderTree, useImagesList;
|
|
1171
|
-
var init_use_uploads_queries = __esm({
|
|
1172
|
-
"src/features/uploads/hooks/use-uploads-queries.ts"() {
|
|
1173
|
-
"use strict";
|
|
1174
|
-
emptyFolderTree = {
|
|
1175
|
-
name: "",
|
|
1176
|
-
path: "",
|
|
1177
|
-
images: [],
|
|
1178
|
-
subfolders: []
|
|
1179
|
-
};
|
|
1180
|
-
useImagesList = (_page = 1, _limit = 50) => ({
|
|
1181
|
-
data: {
|
|
1182
|
-
success: true,
|
|
1183
|
-
data: {
|
|
1184
|
-
data: [],
|
|
1185
|
-
folderTree: emptyFolderTree,
|
|
1186
|
-
pagination: { page: 1, limit: 50, total: 0, totalPages: 0 }
|
|
1187
|
-
}
|
|
1188
|
-
},
|
|
1189
|
-
isLoading: false,
|
|
1190
|
-
isError: false
|
|
1191
|
-
});
|
|
1192
|
-
}
|
|
1193
|
-
});
|
|
1194
|
-
|
|
1195
1169
|
// src/components/ui/collapsible.tsx
|
|
1196
1170
|
function Collapsible({
|
|
1197
1171
|
className,
|
|
@@ -1449,7 +1423,7 @@ function InsertImageUploadsDialogBody({
|
|
|
1449
1423
|
const [altText, setAltText] = (0, import_react5.useState)("");
|
|
1450
1424
|
const [openFolders, setOpenFolders] = (0, import_react5.useState)(/* @__PURE__ */ new Set());
|
|
1451
1425
|
const limit = 100;
|
|
1452
|
-
const { data: imagesData, isLoading } = useImagesList(1, limit);
|
|
1426
|
+
const { data: imagesData, isLoading } = (0, import_uploads_hooks.useImagesList)(1, limit);
|
|
1453
1427
|
const folderTree = imagesData?.data?.folderTree;
|
|
1454
1428
|
const isDisabled = !selectedImage;
|
|
1455
1429
|
const handleImageSelect = React6.useCallback((imageUrl, originalName) => {
|
|
@@ -1744,7 +1718,7 @@ function getDragSelection(event) {
|
|
|
1744
1718
|
}
|
|
1745
1719
|
return range;
|
|
1746
1720
|
}
|
|
1747
|
-
var import_react5, React6, import_LexicalComposerContext2, import_utils14, import_lexical5, import_lucide_react3, import_image, import_jsx_runtime13, getDOMSelection, INSERT_IMAGE_COMMAND;
|
|
1721
|
+
var import_react5, React6, import_LexicalComposerContext2, import_utils14, import_lexical5, import_uploads_hooks, import_lucide_react3, import_image, import_jsx_runtime13, getDOMSelection, INSERT_IMAGE_COMMAND;
|
|
1748
1722
|
var init_images_plugin = __esm({
|
|
1749
1723
|
"src/components/editor/plugins/images-plugin.tsx"() {
|
|
1750
1724
|
"use strict";
|
|
@@ -1761,7 +1735,7 @@ var init_images_plugin = __esm({
|
|
|
1761
1735
|
init_input();
|
|
1762
1736
|
init_label();
|
|
1763
1737
|
init_tabs();
|
|
1764
|
-
|
|
1738
|
+
import_uploads_hooks = require("@thangph2146/nextjs-editor/uploads-hooks");
|
|
1765
1739
|
import_lucide_react3 = require("lucide-react");
|
|
1766
1740
|
import_image = __toESM(require("next/image"), 1);
|
|
1767
1741
|
init_collapsible();
|
|
@@ -19644,12 +19618,12 @@ var init_emoji_list = __esm({
|
|
|
19644
19618
|
});
|
|
19645
19619
|
|
|
19646
19620
|
// src/index.tsx
|
|
19647
|
-
var
|
|
19648
|
-
__export(
|
|
19621
|
+
var src_exports = {};
|
|
19622
|
+
__export(src_exports, {
|
|
19649
19623
|
Editor: () => Editor,
|
|
19650
19624
|
editorThemePath: () => editorThemePath
|
|
19651
19625
|
});
|
|
19652
|
-
module.exports = __toCommonJS(
|
|
19626
|
+
module.exports = __toCommonJS(src_exports);
|
|
19653
19627
|
|
|
19654
19628
|
// src/components/editor/editor-x/editor.tsx
|
|
19655
19629
|
var import_LexicalComposer = require("@lexical/react/LexicalComposer");
|
|
@@ -23284,6 +23258,33 @@ function applyListColorToDom(dom, color) {
|
|
|
23284
23258
|
dom.style.setProperty("--list-marker-color", color, "important");
|
|
23285
23259
|
dom.setAttribute("data-list-color", color);
|
|
23286
23260
|
}
|
|
23261
|
+
function getListColorFromDom(dom) {
|
|
23262
|
+
const attr = dom.getAttribute("data-list-color");
|
|
23263
|
+
if (attr) return attr;
|
|
23264
|
+
const style = dom.style.getPropertyValue("list-style-color") || dom.style.getPropertyValue("--list-marker-color");
|
|
23265
|
+
if (style) return style.trim();
|
|
23266
|
+
const computed = typeof window !== "undefined" ? window.getComputedStyle(dom).getPropertyValue("list-style-color") : "";
|
|
23267
|
+
return computed && computed !== "currentcolor" ? computed.trim() : void 0;
|
|
23268
|
+
}
|
|
23269
|
+
function $convertListWithColorElement(domNode) {
|
|
23270
|
+
if (!(domNode instanceof HTMLElement)) return null;
|
|
23271
|
+
const nodeName = domNode.nodeName.toLowerCase();
|
|
23272
|
+
let listType = "bullet";
|
|
23273
|
+
let start = 1;
|
|
23274
|
+
if (nodeName === "ol") {
|
|
23275
|
+
listType = "number";
|
|
23276
|
+
const startAttr = domNode.getAttribute("start");
|
|
23277
|
+
start = startAttr != null ? parseInt(startAttr, 10) || 1 : 1;
|
|
23278
|
+
} else if (nodeName === "ul") {
|
|
23279
|
+
listType = "bullet";
|
|
23280
|
+
} else {
|
|
23281
|
+
return null;
|
|
23282
|
+
}
|
|
23283
|
+
const node = $createListWithColorNode(listType, start);
|
|
23284
|
+
const color = getListColorFromDom(domNode);
|
|
23285
|
+
if (color) node.setListColor(color);
|
|
23286
|
+
return { node };
|
|
23287
|
+
}
|
|
23287
23288
|
var ListWithColorNode = class _ListWithColorNode extends import_list.ListNode {
|
|
23288
23289
|
__listColor;
|
|
23289
23290
|
constructor(listType, start, key) {
|
|
@@ -23292,6 +23293,12 @@ var ListWithColorNode = class _ListWithColorNode extends import_list.ListNode {
|
|
|
23292
23293
|
static getType() {
|
|
23293
23294
|
return LIST_WITH_COLOR_TYPE;
|
|
23294
23295
|
}
|
|
23296
|
+
static importDOM() {
|
|
23297
|
+
return {
|
|
23298
|
+
ol: () => ({ conversion: $convertListWithColorElement, priority: 0 }),
|
|
23299
|
+
ul: () => ({ conversion: $convertListWithColorElement, priority: 0 })
|
|
23300
|
+
};
|
|
23301
|
+
}
|
|
23295
23302
|
getType() {
|
|
23296
23303
|
return LIST_WITH_COLOR_TYPE;
|
|
23297
23304
|
}
|
|
@@ -23362,6 +23369,11 @@ var ListWithColorNode = class _ListWithColorNode extends import_list.ListNode {
|
|
|
23362
23369
|
return json;
|
|
23363
23370
|
}
|
|
23364
23371
|
};
|
|
23372
|
+
function $createListWithColorNode(listType, start) {
|
|
23373
|
+
return (0, import_lexical13.$applyNodeReplacement)(
|
|
23374
|
+
new ListWithColorNode(listType, start)
|
|
23375
|
+
);
|
|
23376
|
+
}
|
|
23365
23377
|
function $isListWithColorNode(node) {
|
|
23366
23378
|
return node instanceof ListWithColorNode;
|
|
23367
23379
|
}
|