@thangph2146/nextjs-editor 1.0.3 → 1.0.5

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 CHANGED
@@ -26,7 +26,7 @@ Trong `app/layout.tsx` hoặc `_app.tsx`:
26
26
  import "@thangph2146/nextjs-editor/styles.css"
27
27
  ```
28
28
 
29
- **Lưu ý:** Chỉ cần import file trên. Bạn **không** cần thêm `@source` hay cấu hình nào trong `globals.css`. Style editor được đóng gói sẵn scope trong `#editor-x` nên không bị style dự án ghi đè.
29
+ **Lưu ý:** Chỉ cần import file trên. Bạn **không** cần thêm `@source` hay cấu hình nào trong `globals.css`. Style editor được đóng gói sẵn, scope trong `#editor-x` nằm trong layer `nextjs-editor` — nếu app dùng CSS layer cho SCSS/global (xem bên dưới), style editor sẽ không bị ghi đè.
30
30
 
31
31
  ### 2. Dùng component Editor (Client Component)
32
32
 
@@ -61,9 +61,188 @@ export function MyEditor() {
61
61
 
62
62
  ## Ghi chú
63
63
 
64
- - **Ảnh**: Trong bản standalone, dialog chèn ảnh chỉ có tab **URL** và **File**. Để tab **Thư viện** hiển thị ảnh từ API của project, trong `next.config` (webpack) thêm alias: `"@thangph2146/nextjs-editor/uploads-hooks"` → đường dẫn tới file hooks uploads của app (ví dụ `src/features/uploads/hooks/use-uploads-queries.ts`). Hook cần export `useImagesList(page, limit)` trả về `{ data: { data: { folderTree, pagination } }, isLoading }` với `folderTree` có cấu trúc `FolderNode` (name, path, images, subfolders).
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
+ ## Tránh SCSS/CSS của app ghi đè editor
68
+
69
+ Style editor được bọc trong **`@layer nextjs-editor`**. Trong CSS, rule **không nằm trong layer** (unlayered) có độ ưu tiên cao hơn rule trong layer, nên nếu SCSS/global của app là unlayered thì có thể ghi đè list, spacing, typography… bên trong `#editor-x`.
70
+
71
+ **Cách làm:** Đưa toàn bộ SCSS/global của app vào một layer (ví dụ `app`) và khai báo thứ tự layer sao cho `nextjs-editor` đứng **sau** — khi đó style editor sẽ thắng khi specificity tương đương.
72
+
73
+ ### Bước 1: Khai báo thứ tự layer trong app
74
+
75
+ Trong `globals.css` (hoặc file CSS chính), **dòng đầu tiên** khai báo thứ tự layer, thêm `app` và `nextjs-editor` (layer xuất hiện sau có ưu tiên cao hơn):
76
+
77
+ ```css
78
+ @layer legacy, base, components, utilities, app, nextjs-editor;
79
+ ```
80
+
81
+ ### Bước 2: Đưa SCSS/global vào layer `app`
82
+
83
+ Mọi style global/SCSS không nằm trong Tailwind (base, typography, components từ SCSS…) cần nằm trong `@layer app`.
84
+
85
+ **Cách A — Bọc toàn bộ file SCSS chính:**
86
+
87
+ Trong file SCSS chính (ví dụ `src/styles/main.scss`), bọc toàn bộ nội dung trong `@layer app`:
88
+
89
+ ```scss
90
+ @layer app {
91
+ @use "abstracts/variables" as *;
92
+ @use "abstracts/mixins" as *;
93
+ @use "base/base";
94
+ @use "base/typography";
95
+ @use "components/header";
96
+ /* ... các @use khác ... */
97
+ }
98
+ ```
99
+
100
+ **Cách B — Import SCSS từ một file CSS có layer:**
101
+
102
+ Tạo file `src/app/globals-app.css`:
103
+
104
+ ```css
105
+ @layer app {
106
+ @import "../styles/main.scss";
107
+ }
108
+ ```
109
+
110
+ Rồi trong `layout.tsx` import sau `globals.css`: `import "./globals-app.css"`.
111
+
112
+ Sau khi cấu hình, style trong layer `nextjs-editor` (đã load khi import `@thangph2146/nextjs-editor/styles.css`) sẽ có ưu tiên cao hơn layer `app`, nên cấu trúc Tailwind và theme của editor trong `#editor-x` không còn bị SCSS của app ghi đè.
113
+
114
+ ## Cấu hình API thư viện ảnh
115
+
116
+ 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.
117
+
118
+ ### Bước 1: Alias trong Next.js
119
+
120
+ 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:
121
+
122
+ ```ts
123
+ // next.config.ts
124
+ import type { NextConfig } from "next";
125
+ import path from "path";
126
+
127
+ const nextConfig: NextConfig = {
128
+ // ... config khác
129
+ webpack: (config) => {
130
+ config.resolve ??= {};
131
+ config.resolve.alias = {
132
+ ...config.resolve.alias,
133
+ "@thangph2146/nextjs-editor/uploads-hooks": path.resolve(
134
+ __dirname,
135
+ "src/features/uploads/hooks/use-uploads-queries.ts" // đường dẫn tới file hooks của bạn
136
+ ),
137
+ };
138
+ return config;
139
+ },
140
+ };
141
+
142
+ export default nextConfig;
143
+ ```
144
+
145
+ Đường dẫn file có thể khác tùy cấu trúc project (ví dụ `src/lib/uploads-queries.ts`).
146
+
147
+ ### Bước 2: File hooks phải export `useImagesList`
148
+
149
+ File được alias tới phải export hook `useImagesList(page, limit)` với đúng chuẩn:
150
+
151
+ - **Tham số:** `page: number`, `limit: number` (ví dụ `useImagesList(1, 100)`).
152
+ - **Giá trị trả về:** object tương thích với TanStack Query (useQuery), ít nhất:
153
+ - `data`: response từ API, có dạng:
154
+ - `data.data.folderTree`: cây thư mục ảnh (xem kiểu `FolderNode` bên dưới).
155
+ - `data.data.pagination`: `{ page, limit, total, totalPages }` (tùy chọn).
156
+ - `isLoading`: boolean.
157
+
158
+ **Kiểu dữ liệu (TypeScript):**
159
+
160
+ ```ts
161
+ interface ImageItem {
162
+ fileName: string;
163
+ originalName: string;
164
+ size: number;
165
+ mimeType: string;
166
+ url: string; // URL đầy đủ để hiển thị ảnh (img src)
167
+ relativePath: string;
168
+ createdAt: number;
169
+ }
170
+
171
+ interface FolderNode {
172
+ name: string;
173
+ path: string;
174
+ images: ImageItem[];
175
+ subfolders: FolderNode[]; // đệ quy
176
+ }
177
+ ```
178
+
179
+ **Ví dụ hook (gọi API):**
180
+
181
+ ```ts
182
+ // useImagesList gọi GET /api/uploads?page=1&limit=100 (hoặc endpoint của bạn)
183
+ export function useImagesList(page = 1, limit = 50) {
184
+ return useQuery({
185
+ queryKey: ["uploads", "images", page, limit],
186
+ queryFn: async () => {
187
+ const res = await fetch(`/api/uploads?page=${page}&limit=${limit}`);
188
+ const json = await res.json();
189
+ if (!json.success) throw new Error("Failed to fetch images");
190
+ return json; // { success, data: { data: [], folderTree, pagination } }
191
+ },
192
+ });
193
+ }
194
+ ```
195
+
196
+ ### Bước 3: API backend trả về `folderTree`
197
+
198
+ 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.
199
+
200
+ **Ví dụ response:**
201
+
202
+ ```json
203
+ {
204
+ "success": true,
205
+ "data": {
206
+ "data": [],
207
+ "folderTree": {
208
+ "name": "images",
209
+ "path": "images",
210
+ "images": [
211
+ {
212
+ "fileName": "2024/01/15/abc.jpg",
213
+ "originalName": "abc.jpg",
214
+ "size": 12345,
215
+ "mimeType": "image/jpeg",
216
+ "url": "https://example.com/api/uploads/serve/images/2024/01/15/abc.jpg",
217
+ "relativePath": "images/2024/01/15/abc.jpg",
218
+ "createdAt": 1705312800000
219
+ }
220
+ ],
221
+ "subfolders": [
222
+ {
223
+ "name": "2024",
224
+ "path": "images/2024",
225
+ "images": [],
226
+ "subfolders": []
227
+ }
228
+ ]
229
+ },
230
+ "pagination": { "page": 1, "limit": 100, "total": 50, "totalPages": 1 }
231
+ }
232
+ }
233
+ ```
234
+
235
+ - `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).
236
+ - 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).
237
+
238
+ ### Tóm tắt
239
+
240
+ | Việc | Mô tả |
241
+ |------|--------|
242
+ | Alias | `@thangph2146/nextjs-editor/uploads-hooks` → file hooks uploads của project (next.config webpack). |
243
+ | Hook | File đó export `useImagesList(page, limit)` trả về `{ data, isLoading }`, `data.data.folderTree` kiểu `FolderNode`. |
244
+ | API | Backend trả về list ảnh kèm `folderTree` (cây thư mục + ảnh trong từng thư mục). |
245
+
67
246
  ## Build từ source
68
247
 
69
248
  ```bash
package/dist/index.cjs CHANGED
@@ -23258,6 +23258,33 @@ function applyListColorToDom(dom, color) {
23258
23258
  dom.style.setProperty("--list-marker-color", color, "important");
23259
23259
  dom.setAttribute("data-list-color", color);
23260
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
+ }
23261
23288
  var ListWithColorNode = class _ListWithColorNode extends import_list.ListNode {
23262
23289
  __listColor;
23263
23290
  constructor(listType, start, key) {
@@ -23266,6 +23293,12 @@ var ListWithColorNode = class _ListWithColorNode extends import_list.ListNode {
23266
23293
  static getType() {
23267
23294
  return LIST_WITH_COLOR_TYPE;
23268
23295
  }
23296
+ static importDOM() {
23297
+ return {
23298
+ ol: () => ({ conversion: $convertListWithColorElement, priority: 0 }),
23299
+ ul: () => ({ conversion: $convertListWithColorElement, priority: 0 })
23300
+ };
23301
+ }
23269
23302
  getType() {
23270
23303
  return LIST_WITH_COLOR_TYPE;
23271
23304
  }
@@ -23336,6 +23369,11 @@ var ListWithColorNode = class _ListWithColorNode extends import_list.ListNode {
23336
23369
  return json;
23337
23370
  }
23338
23371
  };
23372
+ function $createListWithColorNode(listType, start) {
23373
+ return (0, import_lexical13.$applyNodeReplacement)(
23374
+ new ListWithColorNode(listType, start)
23375
+ );
23376
+ }
23339
23377
  function $isListWithColorNode(node) {
23340
23378
  return node instanceof ListWithColorNode;
23341
23379
  }