@libs-ui/components-inputs-quill2x 0.2.356-42 → 0.2.356-43

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