@libs-ui/components-inputs-quill2x 0.2.356-9 → 0.2.357-1

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
@@ -1,114 +1,599 @@
1
- # Quill2x Rich Text Editor Component
1
+ # @libs-ui/components-inputs-quill2x
2
2
 
3
- `@libs-ui/components-inputs-quill2x` là một trình soạn thảo văn bản giàu tính năng (Rich Text Editor) mạnh mẽ dựa trên Quill 2.0. cung cấp một giao diện linh hoạt, hiện đại và hỗ trợ nhiều tính năng nâng cao như bảng, hình ảnh, video, mention, emoji tùy chỉnh toolbar.
3
+ > Rich Text Editor mạnh mẽ dựa trên Quill 2.x, tích hợp đầy đủ toolbar, mention, emoji, bảng, upload ảnh và validation cho Angular.
4
4
 
5
- ## Tính năng nổi bật
5
+ ## Giới thiệu
6
6
 
7
- - ✒️ **Quill 2.0 Core**: Tận dụng toàn bộ sức mạnhđộ ổn định của Quill 2.
8
- - 🛠️ **Custom Toolbar**: Hỗ trợ nhiều chế độ hiển thị toolbar (`default`, `basic`, `all`, `custom`) và tùy chỉnh vị trí cố định.
9
- - 🖼️ **Media Support**: Tích hợp trình tải lên hình ảnh, video và hỗ trợ resize hình ảnh.
10
- - 🏷️ **Mention Integration**: Hỗ trợ tính năng mention (@người dùng, #thẻ tag) mượt mà.
11
- - 📊 **Table Support**: Hỗ trợ tạo và quản lý bảng chuyên nghiệp với các thao tác dòng/cột.
12
- - 😀 **Emoji Picker**: Tích hợp sẵn bộ chọn emoji.
13
- - ✅ **Validation**: Hỗ trợ các quy tắc kiểm tra bắt buộc, độ dài tối thiểu/tối đa.
14
- - 📱 **Responsive**: Giao diện co giãn tốt và toolbar thông minh (nút xem thêm khi không đủ chỗ).
15
- - 🎨 **Styling**: Tùy chỉnh màu sắc văn bản, màu nền và phông chữ linh hoạt.
7
+ `@libs-ui/components-inputs-quill2x` cung cấp một trình soạn thảo văn bản giàu tính năng (Rich Text Editor) được xây dựng trên nền tảng Quill 2.x. Component hỗ trợ binding hai chiều với object model, toolbar thông minh tự co giãn theo chiều rộng container, cùng nhiều tính năng nâng cao như mention, emoji, bảng, upload ảnh validation tích hợp. Nội dung luôn được lọc qua bộ lọc XSS nội bộ trước khi ghi vào model.
16
8
 
17
- ## Cài đặt
9
+ ## Tính năng
10
+
11
+ - Toolbar thông minh tự tính toán độ rộng, tự động ẩn nút vào menu "Xem thêm" khi không đủ chỗ
12
+ - Hỗ trợ 4 chế độ toolbar: `default`, `basic`, `all`, `custom`
13
+ - Toolbar floating (position fixed) hiển thị theo sự kiện click/mouseenter trên element bất kỳ
14
+ - Tích hợp mention (@user, #tag) với danh sách gợi ý có thể lọc động
15
+ - Bộ chọn emoji tích hợp sẵn
16
+ - Hỗ trợ tạo và quản lý bảng (table) với context menu dòng/cột
17
+ - Upload ảnh từ file hoặc paste từ clipboard, hỗ trợ custom upload function
18
+ - Plugin resize ảnh (tùy chọn)
19
+ - Validation: bắt buộc nhập, độ dài tối thiểu/tối đa
20
+ - Lọc màu gần trắng (near white) khi paste từ Word/nguồn bên ngoài
21
+ - Hỗ trợ nhập tiếng Việt Telex/VNI qua composition event
22
+ - Auto-detect và format URL thành hyperlink khi gõ
23
+ - Lịch sử undo/redo với debounce 2 giây
24
+ - Expose `FunctionsControl` để component cha tương tác trực tiếp với editor
25
+ - XSS filter tự động khi set content
26
+
27
+ ## Khi nào sử dụng
18
28
 
19
- Sử dụng npm hoặc yarn để cài đặt:
29
+ - Khi cần trình soạn thảo văn bản hỗ trợ định dạng (Bold, Italic, danh sách, tiêu đề)
30
+ - Khi xây dựng hệ thống chat hoặc comment cần tính năng mention người dùng
31
+ - Khi cần tích hợp bảng, hình ảnh vào nội dung (CMS, báo cáo, ghi chú)
32
+ - Khi cần trải nghiệm soạn thảo chuyên nghiệp tương đương Slack, Notion
33
+ - Khi cần toolbar floating gắn với một element bên ngoài editor
34
+
35
+ ## Cài đặt
20
36
 
21
37
  ```bash
22
38
  npm install @libs-ui/components-inputs-quill2x
23
39
  ```
24
40
 
25
- ## Cách sử dụng
41
+ > Các package phụ thuộc (`quill2x` = `npm:quill@2.0.3`, `@ssumo/quill-resize-module`, `quill-delta`...) đã khai báo `peerDependencies` trong `package.json` của lib → trình quản lý gói tự cài kèm, không cần cài thủ công.
42
+
43
+ ### ⚠️ Nạp CSS của Quill (BẮT BUỘC — nếu thiếu, editor/toolbar sẽ vỡ giao diện)
44
+
45
+ Quill ship style riêng, **KHÔNG** đi kèm trong bundle của component. Consumer PHẢI tự nạp CSS theme từ package `quill2x`. Khai báo trong `angular.json` / `project.json`:
46
+
47
+ ```jsonc
48
+ // architect.build.options.styles (Angular CLI) HOẶC targets.build.options.styles (Nx)
49
+ "styles": [
50
+ "src/styles.scss",
51
+ "./node_modules/quill2x/dist/quill.snow.css",
52
+ "./node_modules/quill2x/dist/quill.bubble.css"
53
+ ]
54
+ ```
55
+
56
+ > 🔴 `quill.snow.css` là theme mặc định (BẮT BUỘC). Chỉ thêm `quill.bubble.css` nếu dùng theme bubble.
57
+ > Lấy CSS từ package `quill2x` (KHÔNG dùng `quill`) để khớp đúng version Quill 2.x.
58
+ > Nếu gặp cảnh báo CommonJS khi build, thêm `allowedCommonJsDependencies: ["quill2x"]` vào cấu hình build.
26
59
 
27
- ### Import Module
60
+ ## Import
28
61
 
29
62
  ```typescript
30
63
  import { LibsUiComponentsInputsQuill2xComponent } from '@libs-ui/components-inputs-quill2x';
31
64
 
65
+ // Interfaces và types
66
+ import {
67
+ IQuill2xCustomConfig,
68
+ IQuill2xFunctionControlEvent,
69
+ IQuill2xUploadImageConfig,
70
+ IQuill2xBlotRegister,
71
+ IQuill2xSelectionChange,
72
+ IQuill2xTextChange,
73
+ IQuill2xLink,
74
+ IQuill2xToolbarConfig,
75
+ QUILL2X_TYPE_MODE_BAR_CONFIG_OPTION,
76
+ } from '@libs-ui/components-inputs-quill2x';
77
+
78
+ // Utility functions
79
+ import {
80
+ getHTMLFromDeltaOfQuill2x,
81
+ getDeltaOfQuill2xFromHTML,
82
+ isEmptyQuill2x,
83
+ convertStandardList,
84
+ convertStandardListToQuill2x,
85
+ } from '@libs-ui/components-inputs-quill2x';
86
+ ```
87
+
88
+ ## Ví dụ sử dụng
89
+
90
+ ### 1. Cơ bản — binding hai chiều với object model
91
+
92
+ ```typescript
93
+ // component.ts
94
+ import { Component, signal } from '@angular/core';
95
+ import { LibsUiComponentsInputsQuill2xComponent } from '@libs-ui/components-inputs-quill2x';
96
+
32
97
  @Component({
33
98
  standalone: true,
99
+ changeDetection: ChangeDetectionStrategy.OnPush,
34
100
  imports: [LibsUiComponentsInputsQuill2xComponent],
35
- // ...
101
+ templateUrl: './my.component.html',
36
102
  })
37
- export class YourComponent {}
38
- ```
103
+ export class MyComponent {
104
+ protected formData = signal<Record<string, string>>({
105
+ content: '<p>Xin chào! Đây là nội dung ban đầu.</p>',
106
+ });
39
107
 
40
- ### dụ bản
108
+ protected handlerChange(html: string): void {
109
+ // html là nội dung HTML đã qua XSS filter
110
+ console.log('Nội dung mới:', html);
111
+ }
112
+ }
113
+ ```
41
114
 
42
115
  ```html
116
+ <!-- my.component.html -->
43
117
  <libs_ui-components-inputs-quill2x
44
118
  [(item)]="formData"
45
119
  [fieldBind]="'content'"
46
- [placeholder]="'Nhập nội dung tại đây...'"></libs_ui-components-inputs-quill2x>
120
+ [placeholder]="'Nhập nội dung...'"
121
+ (outChange)="handlerChange($event)">
122
+ </libs_ui-components-inputs-quill2x>
47
123
  ```
48
124
 
49
- ### Chế độ Toolbar rút gọn (Basic)
125
+ ### 2. Toàn bộ tính năng — toolbar `all`, resize editor, giới hạn chiều cao
126
+
127
+ ```typescript
128
+ // component.ts
129
+ import { Component, signal } from '@angular/core';
130
+ import { LibsUiComponentsInputsQuill2xComponent, IQuill2xCustomConfig } from '@libs-ui/components-inputs-quill2x';
131
+
132
+ @Component({
133
+ standalone: true,
134
+ changeDetection: ChangeDetectionStrategy.OnPush,
135
+ imports: [LibsUiComponentsInputsQuill2xComponent],
136
+ templateUrl: './my.component.html',
137
+ })
138
+ export class MyComponent {
139
+ protected articleData = signal<Record<string, string>>({
140
+ body: '<h1>Tiêu đề bài viết</h1><p>Nội dung bài viết tại đây.</p>',
141
+ });
142
+
143
+ protected fullConfig: IQuill2xCustomConfig = {
144
+ toolbar: signal({
145
+ type: signal<'all'>('all'),
146
+ }),
147
+ };
148
+
149
+ protected handlerFunctionsControl(ctrl: IQuill2xFunctionControlEvent): void {
150
+ // Lưu lại để sử dụng về sau
151
+ this.editorControl = ctrl;
152
+ }
153
+
154
+ private editorControl?: IQuill2xFunctionControlEvent;
155
+ }
156
+ ```
50
157
 
51
158
  ```html
159
+ <!-- my.component.html -->
52
160
  <libs_ui-components-inputs-quill2x
53
- [(item)]="formData"
54
- [fieldBind]="'summary'"
55
- [quillCustomConfig]="{
56
- toolbar: { type: signal('basic') }
57
- }"></libs_ui-components-inputs-quill2x>
58
- ```
59
-
60
- ## API Reference
61
-
62
- ### Inputs
63
-
64
- | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
65
- | :------------------ | :--------------------- | :---------------------- | :-------------------------------------------------- |
66
- | `item` | `TYPE_OBJECT` | **(Required)** | Object chứa dữ liệu cần bind. |
67
- | `fieldBind` | `string` | **(Required)** | Tên trường trong `item` để bind giá trị HTML. |
68
- | `placeholder` | `string` | `'i18n_import_content'` | Văn bản gợi ý khi editor trống. |
69
- | `readonly` | `boolean` | `false` | Chế độ chỉ đọc. |
70
- | `displayToolbar` | `boolean` | `true` | Hiển thị hoặc ẩn thanh công cụ. |
71
- | `quillCustomConfig` | `IQuill2xCustomConfig` | `undefined` | Cấu hình nâng cao cho toolbar và editor. |
72
- | `dataConfigMention` | `IMentionConfig` | `undefined` | Cấu hình cho tính năng mention. |
73
- | `validRequired` | `IValidRequired` | `undefined` | Quy tắc kiểm tra bắt buộc nhập. |
74
- | `validMinLength` | `IValidLength` | `undefined` | Quy tắc kiểm tra độ dài tối thiểu. |
75
- | `validMaxLength` | `IValidLength` | `undefined` | Quy tắc kiểm tra độ dài tối đa. |
76
- | `resize` | `'vertical' \| 'none'` | `'none'` | Cho phép thay đổi kích thước editor theo chiều dọc. |
77
-
78
- ### Outputs
79
-
80
- | Sự kiện | Kiểu dữ liệu | Mô tả |
81
- | :-------------------- | :----------------------------- | :--------------------------------------------------------------------------------------- |
82
- | `outChange` | `string` | Phát ra nội dung HTML mới khi có thay đổi. |
83
- | `outFocus` | `void` | Phát ra khi editor được focus. |
84
- | `outBlur` | `void` | Phát ra khi editor mất focus. |
85
- | `outFunctionsControl` | `IQuill2xFunctionControlEvent` | Cung cấp các phương thức điều khiển editor (`setContent`, `insertText`, `quill()`, ...). |
161
+ [(item)]="articleData"
162
+ [fieldBind]="'body'"
163
+ [quillCustomConfig]="fullConfig"
164
+ [resize]="'vertical'"
165
+ [minHeightEditorContentDefault]="150"
166
+ [maxHeightEditorContentDefault]="600"
167
+ [resizeImagePlugin]="true"
168
+ [validRequired]="{ isRequired: true, message: 'Vui lòng nhập nội dung' }"
169
+ (outFunctionsControl)="handlerFunctionsControl($event)">
170
+ </libs_ui-components-inputs-quill2x>
171
+ ```
86
172
 
87
- ## Types & Interfaces
173
+ ### 3. Mention — gợi ý @user khi gõ ký tự trigger
88
174
 
89
- ### IQuill2xCustomConfig
175
+ ```typescript
176
+ // component.ts
177
+ import { Component, signal } from '@angular/core';
178
+ import { LibsUiComponentsInputsQuill2xComponent } from '@libs-ui/components-inputs-quill2x';
179
+ import { IMentionConfig } from '@libs-ui/components-inputs-mention';
180
+
181
+ @Component({
182
+ standalone: true,
183
+ changeDetection: ChangeDetectionStrategy.OnPush,
184
+ imports: [LibsUiComponentsInputsQuill2xComponent],
185
+ templateUrl: './my.component.html',
186
+ })
187
+ export class MyComponent {
188
+ protected commentData = signal<Record<string, string>>({ comment: '' });
189
+
190
+ protected mentionConfig: IMentionConfig = {
191
+ items: [
192
+ { id: '1', name: 'Anh Tuấn', username: 'tuan@company.com' },
193
+ { id: '2', name: 'Bảo Ngọc', username: 'ngoc@company.com' },
194
+ { id: '3', name: 'Công Thành', username: 'thanh@company.com' },
195
+ ],
196
+ triggerChar: '@',
197
+ labelKey: 'name',
198
+ mentionFilter: (search: string, items?: Array<Record<string, string>>) => {
199
+ const keyword = search.toLowerCase();
200
+ return items?.filter((item) => item['name'].toLowerCase().includes(keyword));
201
+ },
202
+ mentionSelect: (item: Record<string, string>, triggerChar?: string) => {
203
+ return (triggerChar || '@') + item['name'];
204
+ },
205
+ };
206
+ }
207
+ ```
208
+
209
+ ```html
210
+ <!-- my.component.html -->
211
+ <libs_ui-components-inputs-quill2x
212
+ [(item)]="commentData"
213
+ [fieldBind]="'comment'"
214
+ [dataConfigMention]="mentionConfig"
215
+ [placeholder]="'Nhập bình luận, gõ @ để mention...'">
216
+ </libs_ui-components-inputs-quill2x>
217
+ ```
218
+
219
+ ### 4. Upload ảnh — custom upload function
90
220
 
91
221
  ```typescript
92
- export interface IQuill2xCustomConfig {
222
+ // component.ts
223
+ import { Component, signal } from '@angular/core';
224
+ import { LibsUiComponentsInputsQuill2xComponent, IQuill2xUploadImageConfig } from '@libs-ui/components-inputs-quill2x';
225
+
226
+ @Component({
227
+ standalone: true,
228
+ changeDetection: ChangeDetectionStrategy.OnPush,
229
+ imports: [LibsUiComponentsInputsQuill2xComponent],
230
+ templateUrl: './my.component.html',
231
+ })
232
+ export class MyComponent {
233
+ protected postData = signal<Record<string, string>>({ content: '' });
234
+
235
+ protected uploadConfig: IQuill2xUploadImageConfig = {
236
+ modeCustom: true,
237
+ showIcon: true,
238
+ maxImageSize: 5 * 1024 * 1024, // 5MB
239
+ onlyAcceptImageHttpsLink: true,
240
+ functionUploadImage: async (files: Array<File>): Promise<Array<string | ArrayBuffer>> => {
241
+ // Gọi API upload ảnh thực tế
242
+ const uploadedUrls: string[] = [];
243
+ for (const file of files) {
244
+ const formData = new FormData();
245
+ formData.append('file', file);
246
+ const response = await fetch('/api/upload', { method: 'POST', body: formData });
247
+ const result = await response.json();
248
+ uploadedUrls.push(result.url);
249
+ }
250
+ return uploadedUrls;
251
+ },
252
+ };
253
+ }
254
+ ```
255
+
256
+ ```html
257
+ <!-- my.component.html -->
258
+ <libs_ui-components-inputs-quill2x
259
+ [(item)]="postData"
260
+ [fieldBind]="'content'"
261
+ [uploadImageConfig]="uploadConfig">
262
+ </libs_ui-components-inputs-quill2x>
263
+ ```
264
+
265
+ ### 5. Toolbar floating (position fixed) — toolbar hiển thị theo element trigger
266
+
267
+ ```typescript
268
+ // component.ts
269
+ import { AfterViewInit, Component, ElementRef, signal, ViewChild } from '@angular/core';
270
+ import { LibsUiComponentsInputsQuill2xComponent, IQuill2xCustomConfig } from '@libs-ui/components-inputs-quill2x';
271
+
272
+ @Component({
273
+ standalone: true,
274
+ changeDetection: ChangeDetectionStrategy.OnPush,
275
+ imports: [LibsUiComponentsInputsQuill2xComponent],
276
+ templateUrl: './my.component.html',
277
+ })
278
+ export class MyComponent implements AfterViewInit {
279
+ @ViewChild('triggerBtn') triggerBtn?: ElementRef<HTMLButtonElement>;
280
+
281
+ protected floatingData = signal<Record<string, string>>({ content: '' });
282
+ protected floatingConfig = signal<IQuill2xCustomConfig | undefined>(undefined);
283
+
284
+ ngAfterViewInit(): void {
285
+ if (!this.triggerBtn) return;
286
+ this.floatingConfig.set({
287
+ toolbar: signal({
288
+ type: signal<'default'>('default'),
289
+ positionFixed: signal({
290
+ event: signal<'click'>('click'),
291
+ elementShow: signal(this.triggerBtn.nativeElement),
292
+ position: signal<'bottom'>('bottom'),
293
+ align: signal<'left'>('left'),
294
+ zIndex: signal(9999),
295
+ width: signal(600),
296
+ gapVertical: signal(5),
297
+ gapHorizontal: signal(0),
298
+ }),
299
+ }),
300
+ });
301
+ }
302
+ }
303
+ ```
304
+
305
+ ```html
306
+ <!-- my.component.html -->
307
+ <button #triggerBtn class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
308
+ Mở toolbar
309
+ </button>
310
+ <libs_ui-components-inputs-quill2x
311
+ [(item)]="floatingData"
312
+ [fieldBind]="'content'"
313
+ [quillCustomConfig]="floatingConfig()"
314
+ [minHeightEditorContentDefault]="150">
315
+ </libs_ui-components-inputs-quill2x>
316
+ ```
317
+
318
+ ### 6. Lập trình điều khiển editor qua FunctionsControl
319
+
320
+ ```typescript
321
+ // component.ts
322
+ import { Component, signal } from '@angular/core';
323
+ import { LibsUiComponentsInputsQuill2xComponent, IQuill2xFunctionControlEvent } from '@libs-ui/components-inputs-quill2x';
324
+
325
+ @Component({
326
+ standalone: true,
327
+ changeDetection: ChangeDetectionStrategy.OnPush,
328
+ imports: [LibsUiComponentsInputsQuill2xComponent],
329
+ templateUrl: './my.component.html',
330
+ })
331
+ export class MyComponent {
332
+ protected editorData = signal<Record<string, string>>({ html: '' });
333
+ private editorCtrl?: IQuill2xFunctionControlEvent;
334
+
335
+ protected handlerFunctionsControl(ctrl: IQuill2xFunctionControlEvent): void {
336
+ this.editorCtrl = ctrl;
337
+ }
338
+
339
+ protected async handlerInsertTemplate(): Promise<void> {
340
+ await this.editorCtrl?.setContent('<p>Nội dung template mới được set từ bên ngoài.</p>');
341
+ }
342
+
343
+ protected async handlerValidate(): Promise<void> {
344
+ const isValid = await this.editorCtrl?.checkIsValid();
345
+ console.log('Valid:', isValid);
346
+ }
347
+ }
348
+ ```
349
+
350
+ ```html
351
+ <!-- my.component.html -->
352
+ <libs_ui-components-inputs-quill2x
353
+ [(item)]="editorData"
354
+ [fieldBind]="'html'"
355
+ [validRequired]="{ isRequired: true, message: 'Vui lòng nhập nội dung' }"
356
+ (outFunctionsControl)="handlerFunctionsControl($event)">
357
+ </libs_ui-components-inputs-quill2x>
358
+ <button (click)="handlerInsertTemplate()">Chèn template</button>
359
+ <button (click)="handlerValidate()">Kiểm tra hợp lệ</button>
360
+ ```
361
+
362
+ ## @Input()
363
+
364
+ | Input | Type | Default | Mô tả | Ví dụ |
365
+ |---|---|---|---|---|
366
+ | `item` (model, required) | `TYPE_OBJECT` | — | Object chứa dữ liệu binding. Binding hai chiều qua `[(item)]`. | `[(item)]="formData"` |
367
+ | `fieldBind` (required) | `string` | — | Tên trường trong `item` để đọc/ghi nội dung HTML. | `[fieldBind]="'content'"` |
368
+ | `placeholder` | `string` | `'i18n_import_content'` | Văn bản placeholder khi editor trống. | `[placeholder]="'Nhập nội dung...'"` |
369
+ | `readonly` | `boolean` | `false` | Bật chế độ chỉ đọc, vô hiệu hóa toolbar. | `[readonly]="true"` |
370
+ | `displayToolbar` | `boolean` | `true` | Ẩn/hiện toàn bộ thanh công cụ. | `[displayToolbar]="false"` |
371
+ | `quillCustomConfig` | `IQuill2xCustomConfig` | `undefined` | Cấu hình nâng cao: loại toolbar, position fixed, class tùy chỉnh. | `[quillCustomConfig]="myConfig"` |
372
+ | `dataConfigMention` | `IMentionConfig` | `undefined` | Cấu hình tính năng mention (@user, #tag). | `[dataConfigMention]="mentionConfig"` |
373
+ | `uploadImageConfig` | `IQuill2xUploadImageConfig` | mặc định | Cấu hình upload ảnh: custom mode, kích thước tối đa, function upload. | `[uploadImageConfig]="uploadCfg"` |
374
+ | `label` | `ILabel` | `undefined` | Label hiển thị phía trên editor. | `[label]="{ text: 'Mô tả' }"` |
375
+ | `resize` | `'vertical' \| 'none'` | `'none'` | Cho phép kéo giãn editor theo chiều dọc. | `[resize]="'vertical'"` |
376
+ | `zIndex` | `number` | `1250` | z-index áp dụng cho các popup/modal của editor. | `[zIndex]="2000"` |
377
+ | `autoFocus` | `boolean` | `false` | Tự động focus vào editor khi khởi tạo. | `[autoFocus]="true"` |
378
+ | `focusTimerOnInit` | `number` | `750` | Delay (ms) trước khi thực hiện auto focus. | `[focusTimerOnInit]="300"` |
379
+ | `focusBottom` | `boolean` | `false` | Khi auto focus, đặt con trỏ ở cuối nội dung thay vì đầu. | `[focusBottom]="true"` |
380
+ | `fontSizeDefault` | `number` | `14` | Cỡ chữ mặc định (px). | `[fontSizeDefault]="16"` |
381
+ | `heightEditorContentDefault` | `number` | `undefined` | Chiều cao cố định của vùng soạn thảo (px). | `[heightEditorContentDefault]="300"` |
382
+ | `minHeightEditorContentDefault` | `number` | `undefined` | Chiều cao tối thiểu của vùng soạn thảo (px). | `[minHeightEditorContentDefault]="150"` |
383
+ | `maxHeightEditorContentDefault` | `number` | `undefined` | Chiều cao tối đa của vùng soạn thảo (px). | `[maxHeightEditorContentDefault]="500"` |
384
+ | `validRequired` | `IValidRequired` | `undefined` | Quy tắc bắt buộc nhập. `{ isRequired: true, message: '...' }` | `[validRequired]="{ isRequired: true }"` |
385
+ | `validMinLength` | `IValidLength` | `undefined` | Quy tắc độ dài tối thiểu. `{ length: 10, message: '...' }` | `[validMinLength]="{ length: 20 }"` |
386
+ | `validMaxLength` | `IValidLength` | `undefined` | Quy tắc độ dài tối đa. `{ length: 1000, message: '...' }` | `[validMaxLength]="{ length: 500 }"` |
387
+ | `showErrorLabel` | `boolean` | `true` | Hiển thị label thông báo lỗi validation bên dưới editor. | `[showErrorLabel]="false"` |
388
+ | `showErrorBorder` | `boolean` | `true` | Hiển thị viền đỏ khi validation thất bại. | `[showErrorBorder]="false"` |
389
+ | `ignoreShowBorderErrorToolbar` | `boolean` | `false` | Không áp dụng viền đỏ lên toolbar khi có lỗi. | `[ignoreShowBorderErrorToolbar]="true"` |
390
+ | `blotsRegister` | `Array<IQuill2xBlotRegister>` | `undefined` | Đăng ký custom Quill blots/formats. | `[blotsRegister]="customBlots"` |
391
+ | `templateToolBarPersonalize` | `TemplateRef` | `undefined` | TemplateRef để thêm nút tùy chỉnh vào toolbar. | `[templateToolBarPersonalize]="myTmpl"` |
392
+ | `handlersExpand` | `Array<{title: string; action: () => void}>` | `undefined` | Mở rộng handlers toolbar. Key `title` khớp với tên nút trong toolbar. | `[handlersExpand]="[{title:'myBtn', action: fn}]"` |
393
+ | `resizeImagePlugin` | `boolean` | `false` | Bật plugin cho phép resize ảnh trực tiếp trong editor. | `[resizeImagePlugin]="true"` |
394
+ | `removeNearWhiteColorsOnPaste` | `boolean` | `true` | Tự động xóa màu gần trắng khi paste từ nguồn bên ngoài. | `[removeNearWhiteColorsOnPaste]="false"` |
395
+ | `ignoreShowPopupEditLink` | `boolean` | `false` | Khi `true`, emit `outShowPopupEditLink` thay vì dùng popup mặc định để edit link. | `[ignoreShowPopupEditLink]="true"` |
396
+ | `ignoreCommunicateMicroEventPopup` | `boolean` | `false` | Bỏ qua giao tiếp micro-frontend event cho các popup nội bộ. | `[ignoreCommunicateMicroEventPopup]="true"` |
397
+
398
+ ## @Output()
399
+
400
+ | Output | Type | Mô tả | Handler TS | Binding HTML |
401
+ |---|---|---|---|---|
402
+ | `(outChange)` | `string` | Emit nội dung HTML (đã qua XSS filter) mỗi khi nội dung thay đổi. | `handlerChange(html: string): void { event.stopPropagation?.(); this.savedHtml = html; }` | `(outChange)="handlerChange($event)"` |
403
+ | `(outFocus)` | `void` | Emit khi editor nhận focus. | `handlerFocus(): void { this.isEditing.set(true); }` | `(outFocus)="handlerFocus()"` |
404
+ | `(outBlur)` | `void` | Emit khi editor mất focus. | `handlerBlur(): void { this.isEditing.set(false); }` | `(outBlur)="handlerBlur()"` |
405
+ | `(outFunctionsControl)` | `IQuill2xFunctionControlEvent` | Emit ngay khi editor khởi tạo xong, trả về object chứa các hàm điều khiển editor. | `handlerFunctionsControl(ctrl: IQuill2xFunctionControlEvent): void { this.editorCtrl = ctrl; }` | `(outFunctionsControl)="handlerFunctionsControl($event)"` |
406
+ | `(outSelectionChange)` | `IQuill2xSelectionChange` | Emit khi vị trí con trỏ hoặc vùng chọn thay đổi. | `handlerSelectionChange(e: IQuill2xSelectionChange): void { console.log(e.range); }` | `(outSelectionChange)="handlerSelectionChange($event)"` |
407
+ | `(outTextChange)` | `IQuill2xTextChange` | Emit khi nội dung thay đổi, bao gồm Quill Delta. | `handlerTextChange(e: IQuill2xTextChange): void { console.log(e.delta); }` | `(outTextChange)="handlerTextChange($event)"` |
408
+ | `(outMessageError)` | `string` | Emit thông báo lỗi validation. Chuỗi rỗng khi không có lỗi. | `handlerMessageError(msg: string): void { this.errorMsg.set(msg); }` | `(outMessageError)="handlerMessageError($event)"` |
409
+ | `(outContextMenu)` | `MouseEvent` | Emit khi người dùng click chuột phải vào vùng soạn thảo. | `handlerContextMenu(event: MouseEvent): void { event.stopPropagation(); event.preventDefault(); }` | `(outContextMenu)="handlerContextMenu($event)"` |
410
+ | `(outShowPopupEditLink)` | `{ dataLink: IQuill2xLink; callback: (linkEdit: { title: string; link: string }) => Promise<void> }` | Emit khi cần hiển thị popup edit link tùy chỉnh. Chỉ hoạt động khi `ignoreShowPopupEditLink="true"`. | `handlerShowPopupEditLink(e: { dataLink: IQuill2xLink; callback: Function }): void { this.openCustomLinkDialog(e); }` | `(outShowPopupEditLink)="handlerShowPopupEditLink($event)"` |
411
+
412
+ ## FunctionsControl — Điều khiển editor từ component cha
413
+
414
+ `FunctionsControl` được emit qua `(outFunctionsControl)`. Lưu lại để gọi các hàm bên dưới:
415
+
416
+ ```typescript
417
+ private editorCtrl?: IQuill2xFunctionControlEvent;
418
+
419
+ protected handlerFunctionsControl(ctrl: IQuill2xFunctionControlEvent): void {
420
+ this.editorCtrl = ctrl;
421
+ }
422
+ ```
423
+
424
+ | Method | Signature | Mô tả |
425
+ |---|---|---|
426
+ | `setContent` | `(content: string) => Promise<void>` | Set toàn bộ nội dung HTML của editor (qua XSS filter và convert list). |
427
+ | `insertText` | `(value: string, index?: number, focusLt?: boolean) => Promise<void>` | Chèn plain text tại vị trí con trỏ hoặc index chỉ định. |
428
+ | `insertLink` | `(value: string, url: string, index?: number) => Promise<void>` | Chèn hyperlink vào editor. |
429
+ | `insertImage` | `(content: string, index?: number) => Promise<void>` | Chèn ảnh theo URL vào editor. |
430
+ | `setFontSize` | `(size: number) => Promise<Delta \| undefined>` | Đặt cỡ chữ (px) tại vị trí con trỏ hiện tại. |
431
+ | `setColor` | `(color: string) => Promise<Delta \| undefined>` | Đặt màu chữ tại vị trí con trỏ. |
432
+ | `setBackground` | `(color: string) => Promise<Delta \| undefined>` | Đặt màu nền tại vị trí con trỏ. |
433
+ | `checkIsValid` | `() => Promise<boolean>` | Chạy validation theo các input `valid*` đã cấu hình. Trả về `true` nếu hợp lệ. |
434
+ | `refreshItemValue` | `() => void` | Buộc đọc lại nội dung HTML từ DOM và cập nhật vào `item[fieldBind]`. |
435
+ | `setMessageError` | `(message: string) => Promise<void>` | Hiển thị thông báo lỗi tùy chỉnh bên dưới editor. |
436
+ | `reCalculatorToolbar` | `() => Promise<void>` | Tính toán lại kích thước toolbar (dùng khi container thay đổi kích thước). |
437
+ | `updatePositionToolbar` | `() => Promise<void>` | Cập nhật lại vị trí toolbar floating (dùng khi trigger element di chuyển). |
438
+ | `quill` | `() => Quill2x \| undefined` | Trả về instance Quill gốc để truy cập API Quill trực tiếp. |
439
+
440
+ ## Types & Interfaces
441
+
442
+ ```typescript
443
+ import {
444
+ IQuill2xCustomConfig,
445
+ IQuill2xToolbarConfig,
446
+ IQuill2xUploadImageConfig,
447
+ IQuill2xFunctionControlEvent,
448
+ IQuill2xLink,
449
+ IQuill2xBlotRegister,
450
+ IQuill2xSelectionChange,
451
+ IQuill2xTextChange,
452
+ QUILL2X_TYPE_MODE_BAR_CONFIG_OPTION,
453
+ } from '@libs-ui/components-inputs-quill2x';
454
+ import { WritableSignal } from '@angular/core';
455
+
456
+ // Loại toolbar
457
+ type QUILL2X_TYPE_MODE_BAR_CONFIG_OPTION = 'all' | 'default' | 'basic' | 'custom';
458
+
459
+ // Cấu hình tổng thể editor
460
+ interface IQuill2xCustomConfig {
461
+ classContainer?: WritableSignal<string>;
93
462
  toolbar?: WritableSignal<{
94
- type: WritableSignal<'all' | 'default' | 'basic' | 'custom'>;
463
+ type: WritableSignal<QUILL2X_TYPE_MODE_BAR_CONFIG_OPTION>;
95
464
  positionFixed?: WritableSignal<{
96
465
  event: WritableSignal<'click' | 'mouseenter'>;
97
466
  elementShow: WritableSignal<HTMLElement>;
98
- // ...
467
+ position?: WritableSignal<'top' | 'bottom'>;
468
+ align?: WritableSignal<'left' | 'center' | 'right'>;
469
+ zIndex: WritableSignal<number>;
470
+ width: WritableSignal<number>;
471
+ gapVertical?: WritableSignal<number>;
472
+ gapHorizontal?: WritableSignal<number>;
99
473
  }>;
100
- // ...
474
+ styles?: WritableSignal<Record<string, unknown>>;
475
+ options?: WritableSignal<Array<IQuill2xToolbarConfig>>;
476
+ classCustomContainerToolbar?: WritableSignal<string>;
477
+ lessWidthToolbarRecallCalculator?: WritableSignal<number>;
478
+ }>;
479
+ editor?: WritableSignal<{
480
+ classCustomContainerEditor?: WritableSignal<string>;
101
481
  }>;
102
- // ...
482
+ }
483
+
484
+ // Cấu hình từng nút trên toolbar
485
+ interface IQuill2xToolbarConfig {
486
+ type: string;
487
+ mode?: Array<QUILL2X_TYPE_MODE_BAR_CONFIG_OPTION>;
488
+ width: number;
489
+ display?: boolean;
490
+ classInclude?: string;
491
+ }
492
+
493
+ // Cấu hình upload ảnh
494
+ interface IQuill2xUploadImageConfig {
495
+ modeCustom: boolean; // true = mở popup tùy chỉnh thay vì input file
496
+ showIcon?: boolean; // Hiển thị nút ảnh trên toolbar
497
+ zIndex?: number;
498
+ maxImageSize?: number; // Giới hạn kích thước file (bytes)
499
+ onlyAcceptImageHttpsLink?: boolean; // Chỉ chấp nhận link https
500
+ functionUploadImage?: (files: Array<File>) => Promise<Array<string | ArrayBuffer>>;
501
+ }
502
+
503
+ // API điều khiển editor từ bên ngoài
504
+ interface IQuill2xFunctionControlEvent {
505
+ checkIsValid: () => Promise<boolean>;
506
+ refreshItemValue: () => void;
507
+ setContent: (content: string) => Promise<void>;
508
+ insertText: (value: string, index?: number, focusLt?: boolean) => Promise<void>;
509
+ insertLink: (value: string, url: string, index?: number) => Promise<void>;
510
+ insertImage: (content: string, index?: number) => Promise<void>;
511
+ setFontSize: (size: number) => Promise<Delta | undefined>;
512
+ setColor: (color: string) => Promise<Delta | undefined>;
513
+ setBackground: (color: string) => Promise<Delta | undefined>;
514
+ setMessageError: (message: string) => Promise<void>;
515
+ quill: () => Quill2x | undefined;
516
+ reCalculatorToolbar: () => Promise<void>;
517
+ updatePositionToolbar: () => Promise<void>;
518
+ }
519
+
520
+ // Thông tin link khi click vào hyperlink trong editor
521
+ interface IQuill2xLink {
522
+ title: string;
523
+ url: string;
524
+ range: { index: number; length: number };
525
+ }
526
+
527
+ // Đăng ký custom Quill blot
528
+ interface IQuill2xBlotRegister {
529
+ component: any; // Class Quill blot
530
+ className: string; // CSS class của blot
531
+ style: string; // Inline styles dạng 'key:value;key2:value2'
532
+ ignoreDelete?: boolean; // true = không xóa blot khi nhấn Backspace/Delete
533
+ }
534
+
535
+ // Dữ liệu emit từ outSelectionChange
536
+ interface IQuill2xSelectionChange {
537
+ quill: Quill2x;
538
+ range: Range;
539
+ oldRange: Range;
540
+ source: EmitterSource;
541
+ }
542
+
543
+ // Dữ liệu emit từ outTextChange
544
+ interface IQuill2xTextChange {
545
+ quill: Quill2x;
546
+ delta: Delta;
103
547
  }
104
548
  ```
105
549
 
106
- ## Tech Stack
550
+ ## Utility Functions
107
551
 
108
- - **Core**: Quill 2.0.3, Angular 18+ (Signals)
109
- - **Plugins**: `@ssumo/quill-resize-module` (Resize image)
110
- - **Utilities**: `@libs-ui/utils`, `@libs-ui/components-inputs-mention`
552
+ ```typescript
553
+ import {
554
+ isEmptyQuill2x,
555
+ convertStandardList,
556
+ convertStandardListToQuill2x,
557
+ getHTMLFromDeltaOfQuill2x,
558
+ getDeltaOfQuill2xFromHTML,
559
+ } from '@libs-ui/components-inputs-quill2x';
560
+
561
+ // Kiểm tra editor có nội dung hay không
562
+ const isEmpty = isEmptyQuill2x(quillInstance);
563
+
564
+ // Chuẩn hóa HTML list từ định dạng nội bộ sang HTML chuẩn (để lưu)
565
+ const standardHtml = convertStandardList(rawHtml);
566
+
567
+ // Chuẩn hóa HTML list từ HTML chuẩn sang định dạng Quill (khi load vào editor)
568
+ const quillHtml = convertStandardListToQuill2x(savedHtml);
569
+
570
+ // Lấy HTML từ Quill Delta
571
+ const html = getHTMLFromDeltaOfQuill2x(delta);
572
+
573
+ // Lấy Quill Delta từ HTML string
574
+ const delta = getDeltaOfQuill2xFromHTML(html);
575
+ ```
576
+
577
+ ## Lưu ý quan trọng
578
+
579
+ ⚠️ **item là model bắt buộc**: Phải dùng `[(item)]` (two-way binding). Không truyền object thuần bất biến vì component sẽ ghi trực tiếp vào `item[fieldBind]` khi nội dung thay đổi.
111
580
 
112
- ## License
581
+ ⚠️ **IQuill2xCustomConfig dùng WritableSignal**: Toàn bộ thuộc tính trong `IQuill2xCustomConfig` là `WritableSignal`. Phải khởi tạo với `signal(...)` thay vì truyền giá trị thường. Xem ví dụ mục 2 và 5 ở trên.
582
+
583
+ ⚠️ **position fixed cần ngAfterViewInit**: Khi dùng `positionFixed.elementShow`, element DOM phải tồn tại trước. Khởi tạo config trong `ngAfterViewInit` sau khi `@ViewChild` đã sẵn sàng.
584
+
585
+ ⚠️ **XSS Security**: Nội dung được tự động lọc qua `xssFilter` khi set content qua `setContent()`. Không set content trực tiếp qua Quill API (`quill().clipboard.dangerouslyPasteHTML`) mà không sanitize.
586
+
587
+ ⚠️ **Peer dependency quill2x**: Package `quill2x` phải được cài đặt. Component không hoạt động với `quill` phiên bản khác.
588
+
589
+ ⚠️ **Tính toán toolbar**: Toolbar tự tính toán độ rộng sau khi render. Nếu component cha có animation hoặc thay đổi kích thước, gọi `editorCtrl?.reCalculatorToolbar()` sau khi animation kết thúc để toolbar hiển thị đúng.
590
+
591
+ ⚠️ **convertStandardList khi lưu**: HTML được lưu vào `item[fieldBind]` đã qua `convertStandardList()` để chuẩn hóa định dạng danh sách. Khi hiển thị lại nội dung, dùng component `@libs-ui/components-inputs-quill2x-preview` hoặc apply CSS Quill Snow theme.
592
+
593
+ ## Demo
594
+
595
+ ```bash
596
+ npx nx serve core-ui
597
+ ```
113
598
 
114
- MIT
599
+ Truy cập: http://localhost:4500/inputs/quill2x