@thangph2146/nextjs-editor 1.0.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 ADDED
@@ -0,0 +1,75 @@
1
+ # @your-username/nextjs-editor
2
+
3
+ Rich text editor cho Next.js, xây trên [Lexical](https://lexical.dev/).
4
+
5
+ ## Cài đặt
6
+
7
+ ```bash
8
+ pnpm add @your-username/nextjs-editor
9
+ # hoặc
10
+ npm i @your-username/nextjs-editor
11
+ yarn add @your-username/nextjs-editor
12
+ ```
13
+
14
+ ## Yêu cầu
15
+
16
+ - React 18+
17
+ - Next.js 14+ (cho tính năng ảnh dùng `next/image` / `next/dynamic`)
18
+
19
+ ## Sử dụng
20
+
21
+ ### 1. Import CSS theme
22
+
23
+ Trong `app/layout.tsx` hoặc `_app.tsx`:
24
+
25
+ ```tsx
26
+ import "@your-username/nextjs-editor/styles.css"
27
+ ```
28
+
29
+ ### 2. Dùng component Editor (Client Component)
30
+
31
+ ```tsx
32
+ "use client"
33
+
34
+ import { Editor } from "@your-username/nextjs-editor"
35
+ import type { SerializedEditorState } from "@your-username/nextjs-editor"
36
+
37
+ export function MyEditor() {
38
+ const [state, setState] = useState<SerializedEditorState | undefined>()
39
+
40
+ return (
41
+ <Editor
42
+ editorSerializedState={state}
43
+ onSerializedChange={setState}
44
+ readOnly={false}
45
+ />
46
+ )
47
+ }
48
+ ```
49
+
50
+ ### Props
51
+
52
+ | Prop | Type | Mô tả |
53
+ |------|------|--------|
54
+ | `editorState` | `EditorState` | Trạng thái Lexical (live instance) |
55
+ | `editorSerializedState` | `SerializedEditorState` | Trạng thái đã serialize (JSON) |
56
+ | `onChange` | `(state: EditorState) => void` | Gọi khi nội dung thay đổi (live state) |
57
+ | `onSerializedChange` | `(state: SerializedEditorState) => void` | Gọi khi nội dung thay đổi (JSON, dễ lưu DB) |
58
+ | `readOnly` | `boolean` | Chế độ chỉ đọc |
59
+
60
+ ## Ghi chú
61
+
62
+ - **Ảnh**: Trong bản standalone, dialog chèn ảnh mặc định chỉ có tab **URL**. Để dùng tab thư viện ảnh (upload/folder), app Next.js cần cung cấp implementation uploads và alias module `@your-username/nextjs-editor` tới hooks uploads của app.
63
+ - **Next.js**: Package dùng `next/image` và `next/dynamic` khi chạy trong Next.js; cần cài `next` trong project.
64
+
65
+ ## Build từ source
66
+
67
+ ```bash
68
+ cd nextjs-editor
69
+ pnpm install
70
+ pnpm build
71
+ ```
72
+
73
+ ## License
74
+
75
+ MIT
@@ -0,0 +1,388 @@
1
+ .EditorTheme__code {
2
+ background-color: transparent;
3
+ font-family: Menlo, Consolas, Monaco, monospace;
4
+ display: block;
5
+ padding: 8px 8px 8px 52px;
6
+ line-height: 1.53;
7
+ font-size: 13px;
8
+ margin: 0;
9
+ margin-top: 8px;
10
+ margin-bottom: 8px;
11
+ overflow-x: auto;
12
+ border: 1px solid #ccc;
13
+ position: relative;
14
+ border-radius: 8px;
15
+ tab-size: 2;
16
+ }
17
+ .EditorTheme__code:before {
18
+ content: attr(data-gutter);
19
+ position: absolute;
20
+ background-color: transparent;
21
+ border-right: 1px solid #ccc;
22
+ left: 0;
23
+ top: 0;
24
+ padding: 8px;
25
+ color: #777;
26
+ white-space: pre-wrap;
27
+ text-align: right;
28
+ min-width: 25px;
29
+ }
30
+ .EditorTheme__table {
31
+ border-collapse: collapse;
32
+ border-spacing: 0;
33
+ overflow-y: scroll;
34
+ overflow-x: scroll;
35
+ table-layout: auto; /* auto để width trên td/th được tôn trọng, cột "TT" không bị kéo 243px */
36
+ width: 100%;
37
+ margin: 0px 0px 30px 0px;
38
+ }
39
+
40
+ /* Cho phép cột tôn trọng width đã chỉnh (resize), không bị kéo về min từ theme/class khác */
41
+ .EditorTheme__tableCell,
42
+ .EditorTheme__tableCellHeader {
43
+ min-width: 0 ;
44
+ word-wrap: break-word;
45
+ overflow-wrap: break-word;
46
+ white-space: normal;
47
+ }
48
+
49
+ /* Cột đầu tiên (thứ tự, "TT") mặc định hẹp; resize vẫn ghi đè qua inline style */
50
+ .EditorTheme__table tr > td:first-child,
51
+ .EditorTheme__table tr > th:first-child {
52
+ width: 3rem;
53
+ max-width: 3rem;
54
+ }
55
+ .EditorTheme__tokenComment {
56
+ color: slategray;
57
+ }
58
+ .EditorTheme__tokenPunctuation {
59
+ color: #999;
60
+ }
61
+ .EditorTheme__tokenProperty {
62
+ color: #905;
63
+ }
64
+ .EditorTheme__tokenSelector {
65
+ color: #690;
66
+ }
67
+ .EditorTheme__tokenOperator {
68
+ color: #9a6e3a;
69
+ }
70
+ .EditorTheme__tokenAttr {
71
+ color: #07a;
72
+ }
73
+ .EditorTheme__tokenVariable {
74
+ color: #e90;
75
+ }
76
+ .EditorTheme__tokenFunction {
77
+ color: #dd4a68;
78
+ }
79
+
80
+ .Collapsible__container {
81
+ background-color: var(--background);
82
+ border: 1px solid #ccc;
83
+ border-radius: 0.5rem;
84
+ margin-bottom: 0.5rem;
85
+ }
86
+
87
+ .Collapsible__title{
88
+ padding: 0.25rem;
89
+ padding-left: 1rem;
90
+ position: relative;
91
+ font-weight: bold;
92
+ outline: none;
93
+ cursor: pointer;
94
+ list-style-type: disclosure-closed;
95
+ list-style-position: inside;
96
+ }
97
+
98
+ .Collapsible__title p{
99
+ display: inline-flex;
100
+ }
101
+ .Collapsible__title::marker{
102
+ color: lightgray;
103
+ }
104
+
105
+ /* Màu bullet/số list khi dùng "Đổi màu list" (data-list-color + --list-marker-color do plugin set) */
106
+ .ContentEditable__root ul[data-list-color],
107
+ .ContentEditable__root ol[data-list-color] {
108
+ list-style-color: var(--list-marker-color, currentColor) ;
109
+ }
110
+ .ContentEditable__root ul[data-list-color] li::marker,
111
+ .ContentEditable__root ol[data-list-color] li::marker {
112
+ color: var(--list-marker-color, currentColor) ;
113
+ }
114
+ .Collapsible__container[open] >.Collapsible__title {
115
+ list-style-type: disclosure-open;
116
+ }
117
+
118
+ /* Layout Container Styles */
119
+ [data-lexical-layout-container="true"] {
120
+ display: grid ;
121
+ width: 100% ;
122
+ max-width: 100% ;
123
+ box-sizing: border-box ;
124
+ /* Responsive grid: single column by default */
125
+ grid-template-columns: 1fr ;
126
+ }
127
+
128
+ /* Auto-fit columns with min 400px when container is wide enough */
129
+ @media (min-width: 400px) {
130
+ [data-lexical-layout-container="true"] {
131
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)) ;
132
+ }
133
+ }
134
+
135
+ /* Layout Item Styles - Ensure items don't overflow */
136
+ [data-lexical-layout-item="true"],
137
+ div.border.border-dashed {
138
+ min-width: 0 ;
139
+ width: 100% ;
140
+ max-width: 100% ;
141
+ box-sizing: border-box ;
142
+ overflow: hidden ;
143
+ }
144
+
145
+ /* Hide border and padding when editor is read-only - dùng selector đủ mạnh thay vì !important */
146
+ .ContentEditable__root.editor-readonly [data-lexical-layout-item="true"],
147
+ .ContentEditable__root.editor-readonly div.border.border-dashed,
148
+ .ContentEditable__root.editor-readonly div[class*="border-dashed"],
149
+ .ContentEditable__root[data-editor-readonly="true"] [data-lexical-layout-item="true"],
150
+ .ContentEditable__root[data-editor-readonly="true"] div.border.border-dashed,
151
+ .ContentEditable__root[data-editor-readonly="true"] div[class*="border-dashed"],
152
+ .ContentEditable__root[contenteditable="false"] [data-lexical-layout-item="true"],
153
+ .ContentEditable__root[contenteditable="false"] div.border.border-dashed,
154
+ .ContentEditable__root[contenteditable="false"] div[class*="border-dashed"],
155
+ .ContentEditable__root[aria-readonly="true"] [data-lexical-layout-item="true"],
156
+ .ContentEditable__root[aria-readonly="true"] div.border.border-dashed,
157
+ .ContentEditable__root[aria-readonly="true"] div[class*="border-dashed"] {
158
+ border: none;
159
+ padding: 0;
160
+ }
161
+
162
+ /* Ensure images inside layout items are responsive and respect text-align */
163
+ [data-lexical-layout-item="true"] img,
164
+ div.border.border-dashed img {
165
+ max-width: 100% ;
166
+ width: 100% ;
167
+ height: auto ;
168
+ object-fit: contain ;
169
+ display: inline-block ;
170
+ }
171
+
172
+ /* Support text-align for images in layout items */
173
+ [data-lexical-layout-item="true"] p[style*="text-align: left"],
174
+ div.border.border-dashed p[style*="text-align: left"] {
175
+ text-align: left ;
176
+ }
177
+
178
+ [data-lexical-layout-item="true"] p[style*="text-align: center"],
179
+ div.border.border-dashed p[style*="text-align: center"] {
180
+ text-align: center ;
181
+ }
182
+
183
+ [data-lexical-layout-item="true"] p[style*="text-align: right"],
184
+ div.border.border-dashed p[style*="text-align: right"] {
185
+ text-align: right ;
186
+ }
187
+
188
+ /* Ensure image wrapper respects parent text-align */
189
+ [data-lexical-layout-item="true"] p[style*="text-align: left"] .editor-image,
190
+ div.border.border-dashed p[style*="text-align: left"] .editor-image {
191
+ margin-left: 0 ;
192
+ margin-right: auto ;
193
+ }
194
+
195
+ [data-lexical-layout-item="true"] p[style*="text-align: center"] .editor-image,
196
+ div.border.border-dashed p[style*="text-align: center"] .editor-image {
197
+ margin-left: auto ;
198
+ margin-right: auto ;
199
+ }
200
+
201
+ [data-lexical-layout-item="true"] p[style*="text-align: right"] .editor-image,
202
+ div.border.border-dashed p[style*="text-align: right"] .editor-image {
203
+ margin-left: auto ;
204
+ margin-right: 0 ;
205
+ }
206
+
207
+ /* Ensure all content inside layout items scales properly */
208
+ [data-lexical-layout-item="true"] *,
209
+ div.border.border-dashed * {
210
+ max-width: 100% ;
211
+ }
212
+
213
+ /* Ensure image wrapper scales */
214
+ [data-lexical-layout-item="true"] .editor-image,
215
+ div.border.border-dashed .editor-image {
216
+ max-width: 100% ;
217
+ width: 100% ;
218
+ display: inline-block ;
219
+ }
220
+
221
+ [data-lexical-layout-item="true"] .editor-image > div,
222
+ div.border.border-dashed .editor-image > div {
223
+ max-width: 100% ;
224
+ width: 100% ;
225
+ display: block ;
226
+ }
227
+
228
+ /* Force images to respect container width */
229
+ [data-lexical-layout-item="true"] .editor-image img,
230
+ div.border.border-dashed .editor-image img {
231
+ max-width: 100% ;
232
+ width: 100% ;
233
+ height: auto ;
234
+ object-fit: contain ;
235
+ }
236
+
237
+ /* Generic responsive media inside editor content */
238
+ .ContentEditable__root img {
239
+ max-width: 100% ;
240
+ height: auto ;
241
+ }
242
+
243
+ /* Wrapper và ảnh có kích thước tường minh: ép luôn trong khung, responsive */
244
+ .ContentEditable__root .editor-image .editor-image__sized,
245
+ .ContentEditable__root .editor-image .editor-image__sized img {
246
+ max-width: 100% ;
247
+ width: 100% ;
248
+ height: auto ;
249
+ box-sizing: border-box ;
250
+ }
251
+
252
+ .ContentEditable__root .editor-image,
253
+ .ContentEditable__root .editor-image > div {
254
+ max-width: 100% ;
255
+ }
256
+
257
+ .ContentEditable__root iframe {
258
+ max-width: 100% ;
259
+ width: 100% ;
260
+ }
261
+
262
+ /* Embed wrappers (BlockWithAlignableContents base class) */
263
+ [data-lexical-decorator="true"] > div.user-select-none {
264
+ display: block ;
265
+ width: 100% ;
266
+ max-width: 100% ;
267
+ margin-left: auto ;
268
+ margin-right: auto ;
269
+ overflow: visible ;
270
+ box-sizing: border-box ;
271
+ }
272
+
273
+ [data-lexical-decorator="true"] > div.user-select-none > .relative:not(.editor-embed-frame) {
274
+ display: block ;
275
+ width: 100% ;
276
+ max-width: 100% ;
277
+ height: auto ;
278
+ margin-left: auto ;
279
+ margin-right: auto ;
280
+ box-sizing: border-box ;
281
+ }
282
+
283
+ [data-lexical-decorator="true"] > div.user-select-none iframe,
284
+ [data-lexical-decorator="true"] > div.user-select-none video,
285
+ [data-lexical-decorator="true"] > div.user-select-none embed {
286
+ width: 100% ;
287
+ height: 100% ;
288
+ display: block;
289
+ margin-left: auto;
290
+ margin-right: auto;
291
+ }
292
+
293
+ .ContentEditable__root [data-lexical-decorator="true"] > div.user-select-none {
294
+ width: 100% ;
295
+ max-width: 100% ;
296
+ }
297
+
298
+ .ContentEditable__root
299
+ [data-lexical-decorator="true"]
300
+ > div.user-select-none
301
+ > .relative.inline-block:not(.editor-embed-frame) {
302
+ width: 100% ;
303
+ max-width: 100% ;
304
+ height: auto ;
305
+ display: block ;
306
+ }
307
+
308
+ /* Responsive overrides for resizable embed frames (e.g., YouTube) */
309
+ [data-lexical-decorator="true"] > div.user-select-none > .editor-embed-frame,
310
+ .ContentEditable__root
311
+ [data-lexical-decorator="true"]
312
+ > div.user-select-none
313
+ > .relative.editor-embed-frame {
314
+ display: inline-block ;
315
+ max-width: 100% ;
316
+ margin-left: 0 ;
317
+ margin-right: 0 ;
318
+ box-sizing: border-box ;
319
+ vertical-align: top;
320
+ }
321
+
322
+ .editor-embed-frame {
323
+ position: relative;
324
+ box-sizing: border-box;
325
+ min-width: 0;
326
+ vertical-align: top;
327
+ line-height: 0;
328
+ height: auto;
329
+ }
330
+
331
+ .editor-embed-frame--full {
332
+ display: block ;
333
+ width: 100% ;
334
+ }
335
+
336
+ .editor-embed-frame--inline {
337
+ display: inline-block ;
338
+ }
339
+
340
+ .editor-embed-frame--inline {
341
+ max-width: 100%;
342
+ }
343
+
344
+ [data-lexical-align="left"] .editor-embed-frame--inline,
345
+ [data-lexical-align="start"] .editor-embed-frame--inline {
346
+ margin-left: 0 ;
347
+ margin-right: auto ;
348
+ }
349
+
350
+ [data-lexical-align="center"] .editor-embed-frame--inline {
351
+ margin-left: auto ;
352
+ margin-right: auto ;
353
+ }
354
+
355
+ [data-lexical-align="right"] .editor-embed-frame--inline,
356
+ [data-lexical-align="end"] .editor-embed-frame--inline {
357
+ margin-left: auto ;
358
+ margin-right: 0 ;
359
+ }
360
+
361
+ .editor-embed-frame iframe {
362
+ position: absolute;
363
+ inset: 0;
364
+ width: 100% ;
365
+ height: 100% ;
366
+ }
367
+
368
+ /* Remove border for readonly editor surface */
369
+ #editor-x .ContentEditable__root[contenteditable="false"],
370
+ #editor-x .ContentEditable__root[aria-readonly="true"] {
371
+ border: none ;
372
+ }
373
+
374
+ /* Admin editor: ép ảnh và wrapper luôn responsive trong #editor-x */
375
+ #editor-x .ContentEditable__root {
376
+ max-width: 100% ;
377
+ min-width: 0 ;
378
+ }
379
+
380
+ #editor-x .ContentEditable__root .editor-image,
381
+ #editor-x .ContentEditable__root .editor-image > div,
382
+ #editor-x .ContentEditable__root .editor-image .editor-image__sized,
383
+ #editor-x .ContentEditable__root .editor-image .editor-image__sized img {
384
+ max-width: 100% ;
385
+ width: 100% ;
386
+ height: auto ;
387
+ box-sizing: border-box ;
388
+ }