@thangph2146/nextjs-editor
v1.0.10
Published
Rich text editor for Next.js built on Lexical
Maintainers
Readme
@thangph2146/nextjs-editor
Rich text editor cho Next.js, xây trên Lexical.
Cài đặt
pnpm add @thangph2146/nextjs-editor
# hoặc
npm i @thangph2146/nextjs-editor
yarn add @thangph2146/nextjs-editorYêu cầu
- React 18+
- Next.js 14+ (cho tính năng ảnh dùng
next/image/next/dynamic)
Sử dụng
1. Import CSS (bắt buộc)
Trong app/layout.tsx hoặc _app.tsx:
import "@thangph2146/nextjs-editor/styles.css"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 và không bọc trong @layer (unlayered). Để style editor không bị SCSS app ghi đè, nên import styles.css sau cùng (sau globals và SCSS của app).
2. Dùng component Editor (Client Component)
"use client"
import { Editor } from "@thangph2146/nextjs-editor"
import type { SerializedEditorState } from "@thangph2146/nextjs-editor"
export function MyEditor() {
const [state, setState] = useState<SerializedEditorState | undefined>()
return (
<Editor
editorSerializedState={state}
onSerializedChange={setState}
readOnly={false}
/>
)
}Props
| Prop | Type | Mô tả |
|------|------|--------|
| editorState | EditorState | Trạng thái Lexical (live instance) |
| editorSerializedState | SerializedEditorState | Trạng thái đã serialize (JSON) |
| onChange | (state: EditorState) => void | Gọi khi nội dung thay đổi (live state) |
| onSerializedChange | (state: SerializedEditorState) => void | Gọi khi nội dung thay đổi (JSON, dễ lưu DB) |
| readOnly | boolean | Chế độ chỉ đọc |
Ghi chú
- Ả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 bên dưới.
- Next.js: Package dùng
next/imagevànext/dynamickhi chạy trong Next.js; cần càinexttrong project.
Tránh SCSS/CSS của app ghi đè editor
Style editor được build không bọc trong @layer (unlayered). Nên import @thangph2146/nextjs-editor/styles.css sau cùng trong layout.tsx (sau globals và SCSS của app) để style editor có thứ tự load sau và dễ thắng khi specificity ngang nhau. App có thể ghi đè style editor nếu dùng selector cùng hoặc cao hơn specificity (vd. trùng #editor-x hoặc class trong scope).
Mọi style global/SCSS không nằm trong Tailwind cần nằm trong @layer app. Lưu ý: Trong Sass, @use không được phép bên trong @layer, nên không bọc trực tiếp nội dung có @use trong một block @layer app { }.
Cách khuyến nghị — File wrapper dùng @import:
Tạo file SCSS mới (ví dụ src/styles/app-layer.scss) chỉ dùng @import (Sass cho phép @import trong @layer):
@layer app {
@import "main";
}Trong layout.tsx (hoặc nơi đang import file SCSS chính), đổi sang import file wrapper:
import "../styles/app-layer.scss";Cách khác — File CSS import SCSS (nếu bundler hỗ trợ):
Tạo file src/app/globals-app.css:
@layer app {
@import "../styles/main.scss";
}Rồi trong layout.tsx import sau globals.css: import "./globals-app.css". (Một số bundler có thể không compile .scss khi import từ .css.)
Sau khi cấu hình (nếu dùng), style editor (đã load khi import @thangph2146/nextjs-editor/styles.css) có thứ tự sau SCSS trong @layer app; cấu trúc Tailwind và theme của editor trong #editor-x có thể bị app ghi đè nếu selector app có specificity cao hơn.
Cấu hình API thư viện ảnh
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.
Bước 1: Alias trong Next.js
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:
// next.config.ts
import type { NextConfig } from "next";
import path from "path";
const nextConfig: NextConfig = {
// ... config khác
webpack: (config) => {
config.resolve ??= {};
config.resolve.alias = {
...config.resolve.alias,
"@thangph2146/nextjs-editor/uploads-hooks": path.resolve(
__dirname,
"src/features/uploads/hooks/use-uploads-queries.ts" // đường dẫn tới file hooks của bạn
),
};
return config;
},
};
export default nextConfig;Đường dẫn file có thể khác tùy cấu trúc project (ví dụ src/lib/uploads-queries.ts).
Bước 2: File hooks phải export useImagesList
File được alias tới phải export hook useImagesList(page, limit) với đúng chuẩn:
- Tham số:
page: number,limit: number(ví dụuseImagesList(1, 100)). - Giá trị trả về: object tương thích với TanStack Query (useQuery), ít nhất:
data: response từ API, có dạng:data.data.folderTree: cây thư mục ảnh (xem kiểuFolderNodebên dưới).data.data.pagination:{ page, limit, total, totalPages }(tùy chọn).
isLoading: boolean.
Kiểu dữ liệu (TypeScript):
interface ImageItem {
fileName: string;
originalName: string;
size: number;
mimeType: string;
url: string; // URL đầy đủ để hiển thị ảnh (img src)
relativePath: string;
createdAt: number;
}
interface FolderNode {
name: string;
path: string;
images: ImageItem[];
subfolders: FolderNode[]; // đệ quy
}Ví dụ hook (gọi API):
// useImagesList gọi GET /api/uploads?page=1&limit=100 (hoặc endpoint của bạn)
export function useImagesList(page = 1, limit = 50) {
return useQuery({
queryKey: ["uploads", "images", page, limit],
queryFn: async () => {
const res = await fetch(`/api/uploads?page=${page}&limit=${limit}`);
const json = await res.json();
if (!json.success) throw new Error("Failed to fetch images");
return json; // { success, data: { data: [], folderTree, pagination } }
},
});
}Bước 3: API backend trả về folderTree
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.
Ví dụ response:
{
"success": true,
"data": {
"data": [],
"folderTree": {
"name": "images",
"path": "images",
"images": [
{
"fileName": "2024/01/15/abc.jpg",
"originalName": "abc.jpg",
"size": 12345,
"mimeType": "image/jpeg",
"url": "https://example.com/api/uploads/serve/images/2024/01/15/abc.jpg",
"relativePath": "images/2024/01/15/abc.jpg",
"createdAt": 1705312800000
}
],
"subfolders": [
{
"name": "2024",
"path": "images/2024",
"images": [],
"subfolders": []
}
]
},
"pagination": { "page": 1, "limit": 100, "total": 50, "totalPages": 1 }
}
}folderTree.url(hoặc mỗiImageItem.url) phải là URL mà trình duyệt tải được (absolute hoặc same-origin).- 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).
Tóm tắt
| Việc | Mô tả |
|------|--------|
| Alias | @thangph2146/nextjs-editor/uploads-hooks → file hooks uploads của project (next.config webpack). |
| Hook | File đó export useImagesList(page, limit) trả về { data, isLoading }, data.data.folderTree kiểu FolderNode. |
| API | Backend trả về list ảnh kèm folderTree (cây thư mục + ảnh trong từng thư mục). |
Build từ source
cd nextjs-editor
pnpm install
pnpm buildCập nhật thư viện lên npm
Khi đã sửa code và muốn publish phiên bản mới:
1. Tăng version trong package.json
Dùng Semantic Versioning:
- Patch (sửa lỗi):
1.0.0→1.0.1npm version patch - Minor (tính năng mới, không phá tương thích):
1.0.0→1.1.0npm version minor - Major (thay đổi breaking):
1.0.0→2.0.0npm version major
Hoặc sửa tay trường "version" trong package.json.
2. Build lại
cd nextjs-editor
pnpm build3. Publish lên npm
# Package scope → cần --access public (chỉ lần đầu hoặc nếu chưa set)
npm publish --access publicNếu đã publish public trước đó, chỉ cần:
npm publish4. (Tùy chọn) Commit & push tag
git add package.json
git commit -m "chore: bump version to x.y.z"
git push origin main
# Tag do npm version tạo (vd. v1.0.1):
git push origin v1.0.1Người dùng cài phiên bản mới:
# Luôn lấy bản mới nhất trong dải version đã khai báo (^1.0.0)
pnpm update @thangph2146/nextjs-editor
# Hoặc cài đúng version cụ thể
pnpm add @thangph2146/[email protected]License
MIT
