@libs-ui/components-inputs-quill 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,3 +1,527 @@
1
- # inputs-quill
1
+ # @libs-ui/components-inputs-quill
2
2
 
3
- This library was generated with [Nx](https://nx.dev).
3
+ > Trình soạn thảo văn bản giàu tính năng (Rich Text Editor) dựa trên Quill.js, tích hợp sẵn toolbar thích ứng, hỗ trợ mention, upload ảnh, validation và bảo mật XSS.
4
+
5
+ ## Giới thiệu
6
+
7
+ `@libs-ui/components-inputs-quill` là một Angular component bọc xung quanh thư viện Quill.js, cung cấp trải nghiệm soạn thảo văn bản chuyên nghiệp với toolbar tự động tính toán độ rộng và ẩn/hiện các nút thừa vào menu "Xem thêm". Component hỗ trợ đầy đủ các tính năng định dạng (Bold, Italic, Heading, Font, Color…), tích hợp mention (@user), upload hình ảnh, link, emoji và validation nội dung. Mọi nội dung đầu ra đều được lọc qua bộ lọc XSS tích hợp để đảm bảo an toàn.
8
+
9
+ ## Tính năng
10
+
11
+ - ✅ Toolbar đầy đủ: Undo/Redo, Font family, Font size, Color, Background, Bold, Italic, Underline, Strike-through, Align, Indent, List, Blockquote, Link, Emoji, Image
12
+ - ✅ Toolbar thích ứng: tự động tính toán độ rộng container và ẩn nút thừa vào "Xem thêm"
13
+ - ✅ 3 chế độ toolbar: `default` (đầy đủ), `basic` (cơ bản), `custom` (tự cấu hình)
14
+ - ✅ Hỗ trợ Mention (`@user`) thông qua `dataConfigMention`
15
+ - ✅ Upload hình ảnh từ file hoặc qua clipboard paste
16
+ - ✅ Validation: bắt buộc nhập, độ dài tối thiểu/tối đa
17
+ - ✅ Bảo mật XSS: tất cả nội dung input/output đều qua `xssFilter`
18
+ - ✅ Hỗ trợ chế độ readonly
19
+ - ✅ Custom blots (đăng ký định dạng Quill tùy chỉnh)
20
+ - ✅ Lấy API điều khiển qua `outFunctionsControl` (setContent, insertText, insertLink, insertImage, setFontSize, v.v.)
21
+ - ✅ Hỗ trợ `heightAuto` — tự động thay đổi chiều cao theo nội dung
22
+ - ✅ Two-way binding qua `[(item)]`
23
+ - ✅ Standalone component, Angular Signals, OnPush
24
+
25
+ ## Khi nào sử dụng
26
+
27
+ - Khi cần trình soạn thảo văn bản có định dạng trong form (mô tả, nội dung bài viết, ghi chú)
28
+ - Khi cần hỗ trợ đề cập người dùng (`@mention`) trong editor
29
+ - Khi cần cho phép người dùng upload và chèn hình ảnh vào nội dung
30
+ - Khi xây dựng hệ thống CMS, chat, hoặc quản lý nội dung phức tạp
31
+ - Khi cần kiểm soát validation nội dung nhập vào (bắt buộc, min/max length)
32
+
33
+ ## Cài đặt
34
+
35
+ ```bash
36
+ npm install @libs-ui/components-inputs-quill
37
+ ```
38
+
39
+ ## Import
40
+
41
+ ```typescript
42
+ import { LibsUiComponentsInputsQuillComponent } from '@libs-ui/components-inputs-quill';
43
+
44
+ // Các interface và types
45
+ import {
46
+ IQuillFunctionControlEvent,
47
+ IQuillUploadImageConfig,
48
+ IQuillBlotRegister,
49
+ IQuillLink,
50
+ IQuillSelectionChange,
51
+ IQuillTextChange,
52
+ IQuillToolbarConfig,
53
+ TYPE_MODE_BAR_CONFIG_OPTION,
54
+ } from '@libs-ui/components-inputs-quill';
55
+
56
+ // Các utility functions
57
+ import {
58
+ getHTMLFromDeltaOfQuill,
59
+ convertHtmlToDivBlocks,
60
+ getDeltaOfQuillFromHTML,
61
+ processPasteData,
62
+ insertContentWithRange,
63
+ } from '@libs-ui/components-inputs-quill';
64
+ ```
65
+
66
+ ## Ví dụ sử dụng
67
+
68
+ ### 1. Cơ bản — Two-way binding
69
+
70
+ ```typescript
71
+ // component.ts
72
+ import { Component, signal } from '@angular/core';
73
+ import { LibsUiComponentsInputsQuillComponent } from '@libs-ui/components-inputs-quill';
74
+
75
+ @Component({
76
+ standalone: true,
77
+ changeDetection: ChangeDetectionStrategy.OnPush,
78
+ imports: [LibsUiComponentsInputsQuillComponent],
79
+ template: `
80
+ <libs_ui-components-inputs-quill
81
+ [(item)]="formData"
82
+ [fieldNameBind]="'content'"
83
+ [placeholder]="'Nhập nội dung tại đây...'"
84
+ ></libs_ui-components-inputs-quill>
85
+ `,
86
+ })
87
+ export class MyComponent {
88
+ protected formData = signal<Record<string, string>>({ content: '' });
89
+ }
90
+ ```
91
+
92
+ ### 2. Với Validation và Label
93
+
94
+ ```typescript
95
+ // component.ts
96
+ import { Component, signal } from '@angular/core';
97
+ import { LibsUiComponentsInputsQuillComponent } from '@libs-ui/components-inputs-quill';
98
+ import { IValidRequired, IValidLength } from '@libs-ui/components-inputs-valid';
99
+
100
+ @Component({
101
+ standalone: true,
102
+ changeDetection: ChangeDetectionStrategy.OnPush,
103
+ imports: [LibsUiComponentsInputsQuillComponent],
104
+ templateUrl: './my.component.html',
105
+ })
106
+ export class MyComponent {
107
+ protected formData = signal<Record<string, string>>({ description: '' });
108
+
109
+ protected validRequired: IValidRequired = {
110
+ isRequired: true,
111
+ message: 'Vui lòng nhập mô tả',
112
+ };
113
+
114
+ protected validMinLength: IValidLength = {
115
+ length: 10,
116
+ message: 'Mô tả phải có ít nhất 10 ký tự',
117
+ };
118
+
119
+ protected validMaxLength: IValidLength = {
120
+ length: 5000,
121
+ message: 'Mô tả không được vượt quá 5000 ký tự',
122
+ };
123
+ }
124
+ ```
125
+
126
+ ```html
127
+ <!-- my.component.html -->
128
+ <libs_ui-components-inputs-quill
129
+ [(item)]="formData"
130
+ [fieldNameBind]="'description'"
131
+ [label]="{ title: 'Mô tả chi tiết', isRequired: true }"
132
+ [validRequired]="validRequired"
133
+ [validMinLength]="validMinLength"
134
+ [validMaxLength]="validMaxLength"
135
+ [showErrorLabel]="true"
136
+ [showErrorBorder]="true"
137
+ ></libs_ui-components-inputs-quill>
138
+ ```
139
+
140
+ ### 3. Với Mention và Upload hình ảnh
141
+
142
+ ```typescript
143
+ // component.ts
144
+ import { Component, signal } from '@angular/core';
145
+ import { LibsUiComponentsInputsQuillComponent, IQuillUploadImageConfig } from '@libs-ui/components-inputs-quill';
146
+ import { IMentionConfig } from '@libs-ui/components-inputs-mention';
147
+
148
+ @Component({
149
+ standalone: true,
150
+ changeDetection: ChangeDetectionStrategy.OnPush,
151
+ imports: [LibsUiComponentsInputsQuillComponent],
152
+ templateUrl: './my.component.html',
153
+ })
154
+ export class MyComponent {
155
+ protected formData = signal<Record<string, string>>({ content: '' });
156
+
157
+ protected mentionConfig: IMentionConfig = {
158
+ items: [
159
+ { id: '1', name: 'Nguyễn Văn An', username: 'an@example.com' },
160
+ { id: '2', name: 'Trần Thị Bình', username: 'binh@example.com' },
161
+ { id: '3', name: 'Lê Văn Cường', username: 'cuong@example.com' },
162
+ ],
163
+ triggerChar: '@',
164
+ labelKey: 'name',
165
+ mentionFilter: (search, items) =>
166
+ items?.filter((item) => item.name.toLowerCase().includes(search.toLowerCase())),
167
+ mentionSelect: (item, triggerChar) => `${triggerChar || '@'}${item.name}`,
168
+ };
169
+
170
+ protected uploadImageConfig: IQuillUploadImageConfig = {
171
+ modeCustom: false,
172
+ showIcon: true,
173
+ maxImageSize: 5 * 1024 * 1024, // 5MB
174
+ onlyAcceptImageHttpsLink: false,
175
+ functionUploadImage: async (files: File[]) => {
176
+ // Gọi API upload file và trả về array URL
177
+ const urls: string[] = [];
178
+ for (const file of files) {
179
+ const url = await this.uploadToServer(file);
180
+ urls.push(url);
181
+ }
182
+ return urls;
183
+ },
184
+ };
185
+
186
+ private async uploadToServer(file: File): Promise<string> {
187
+ // Implement API call thực tế
188
+ return 'https://example.com/images/uploaded.jpg';
189
+ }
190
+ }
191
+ ```
192
+
193
+ ```html
194
+ <!-- my.component.html -->
195
+ <libs_ui-components-inputs-quill
196
+ [(item)]="formData"
197
+ [fieldNameBind]="'content'"
198
+ [dataConfigMention]="mentionConfig"
199
+ [uploadImageConfig]="uploadImageConfig"
200
+ [placeholder]="'Gõ @ để mention người dùng...'"
201
+ ></libs_ui-components-inputs-quill>
202
+ ```
203
+
204
+ ### 4. Lấy API điều khiển qua outFunctionsControl
205
+
206
+ ```typescript
207
+ // component.ts
208
+ import { Component, signal } from '@angular/core';
209
+ import { LibsUiComponentsInputsQuillComponent, IQuillFunctionControlEvent } from '@libs-ui/components-inputs-quill';
210
+
211
+ @Component({
212
+ standalone: true,
213
+ changeDetection: ChangeDetectionStrategy.OnPush,
214
+ imports: [LibsUiComponentsInputsQuillComponent],
215
+ templateUrl: './my.component.html',
216
+ })
217
+ export class MyComponent {
218
+ protected formData = signal<Record<string, string>>({ content: '' });
219
+
220
+ private quillControl: IQuillFunctionControlEvent | undefined;
221
+
222
+ protected handlerFunctionsControl(event: IQuillFunctionControlEvent): void {
223
+ event.stopPropagation?.();
224
+ this.quillControl = event;
225
+ }
226
+
227
+ protected async handlerSetContent(event: Event): Promise<void> {
228
+ event.stopPropagation();
229
+ await this.quillControl?.setContent('<p>Nội dung mới được đặt từ bên ngoài</p>');
230
+ }
231
+
232
+ protected async handlerInsertText(event: Event): Promise<void> {
233
+ event.stopPropagation();
234
+ await this.quillControl?.insertText('Văn bản được chèn');
235
+ }
236
+
237
+ protected async handlerValidate(event: Event): Promise<void> {
238
+ event.stopPropagation();
239
+ const isValid = await this.quillControl?.checkIsValid();
240
+ console.log('Is valid:', isValid);
241
+ }
242
+
243
+ protected async handlerInsertLink(event: Event): Promise<void> {
244
+ event.stopPropagation();
245
+ await this.quillControl?.insertLink('Xem chi tiết', 'https://example.com');
246
+ }
247
+ }
248
+ ```
249
+
250
+ ```html
251
+ <!-- my.component.html -->
252
+ <libs_ui-components-inputs-quill
253
+ [(item)]="formData"
254
+ [fieldNameBind]="'content'"
255
+ (outFunctionsControl)="handlerFunctionsControl($event)"
256
+ ></libs_ui-components-inputs-quill>
257
+
258
+ <button (click)="handlerSetContent($event)">Đặt nội dung</button>
259
+ <button (click)="handlerInsertText($event)">Chèn văn bản</button>
260
+ <button (click)="handlerInsertLink($event)">Chèn link</button>
261
+ <button (click)="handlerValidate($event)">Kiểm tra hợp lệ</button>
262
+ ```
263
+
264
+ ### 5. Chế độ toolbar tùy chỉnh
265
+
266
+ ```typescript
267
+ // component.ts
268
+ import { Component, signal } from '@angular/core';
269
+ import { LibsUiComponentsInputsQuillComponent, IQuillToolbarConfig, TYPE_MODE_BAR_CONFIG_OPTION } from '@libs-ui/components-inputs-quill';
270
+
271
+ @Component({
272
+ standalone: true,
273
+ changeDetection: ChangeDetectionStrategy.OnPush,
274
+ imports: [LibsUiComponentsInputsQuillComponent],
275
+ templateUrl: './my.component.html',
276
+ })
277
+ export class MyComponent {
278
+ protected formData = signal<Record<string, string>>({ content: '' });
279
+
280
+ protected toolbarConfig: { type: TYPE_MODE_BAR_CONFIG_OPTION; config?: IQuillToolbarConfig[] } = {
281
+ type: 'basic',
282
+ };
283
+ }
284
+ ```
285
+
286
+ ```html
287
+ <!-- my.component.html -->
288
+ <libs_ui-components-inputs-quill
289
+ [(item)]="formData"
290
+ [fieldNameBind]="'content'"
291
+ [toolbarConfig]="toolbarConfig"
292
+ [isShowToolBar]="true"
293
+ ></libs_ui-components-inputs-quill>
294
+ ```
295
+
296
+ ### 6. Readonly mode
297
+
298
+ ```html
299
+ <libs_ui-components-inputs-quill
300
+ [(item)]="formData"
301
+ [fieldNameBind]="'content'"
302
+ [readonly]="true"
303
+ [isShowToolBar]="false"
304
+ ></libs_ui-components-inputs-quill>
305
+ ```
306
+
307
+ ### 7. Auto focus và heightAuto
308
+
309
+ ```html
310
+ <libs_ui-components-inputs-quill
311
+ [(item)]="formData"
312
+ [fieldNameBind]="'content'"
313
+ [autoFocus]="true"
314
+ [focusBottom]="true"
315
+ [heightAuto]="true"
316
+ ></libs_ui-components-inputs-quill>
317
+ ```
318
+
319
+ ## @Input()
320
+
321
+ | Input | Type | Default | Mô tả | Ví dụ |
322
+ |---|---|---|---|---|
323
+ | `item` | `TYPE_OBJECT` (model) | `undefined` | Object chứa dữ liệu two-way binding. Dùng `[(item)]` để binding hai chiều. | `[(item)]="formData"` |
324
+ | `fieldNameBind` | `string` | `'value'` | Tên trường trong `item` dùng để đọc/ghi nội dung HTML của editor. | `[fieldNameBind]="'content'"` |
325
+ | `isShowToolBar` | `boolean` | `true` | Hiển thị hoặc ẩn thanh toolbar. | `[isShowToolBar]="false"` |
326
+ | `isToolbarPositionFixed` | `boolean` | `false` | Đặt toolbar ở vị trí cố định (fixed) thay vì inline. | `[isToolbarPositionFixed]="true"` |
327
+ | `classIncludeToolbar` | `string` | `''` | CSS class bổ sung cho container toolbar. | `[classIncludeToolbar]="'border-b'"` |
328
+ | `stylesIncludeToolbar` | `TYPE_OBJECT` | `undefined` | Style object bổ sung cho toolbar. | `[stylesIncludeToolbar]="{ background: '#f5f5f5' }"` |
329
+ | `toolbarConfig` | `{ type: TYPE_MODE_BAR_CONFIG_OPTION; config?: IQuillToolbarConfig[] }` | `undefined` | Cấu hình chế độ toolbar. `type` có thể là `'default'`, `'basic'` hoặc `'custom'`. | `[toolbarConfig]="{ type: 'basic' }"` |
330
+ | `placeholder` | `string` | `'i18n_import_content'` | Placeholder text khi editor trống. | `[placeholder]="'Nhập nội dung...'"` |
331
+ | `label` | `ILabel` | `undefined` | Cấu hình label hiển thị bên trên editor. | `[label]="{ title: 'Nội dung', isRequired: true }"` |
332
+ | `autoUpdateValueWhenTextChange` | `boolean` | `true` | Tự động cập nhật `item[fieldNameBind]` mỗi khi nội dung thay đổi. Đặt `false` để tối ưu hiệu năng và chỉ lấy giá trị khi cần (gọi `refreshItemValue()`). | `[autoUpdateValueWhenTextChange]="false"` |
333
+ | `readonly` | `boolean` | `undefined` | Chế độ chỉ đọc, không cho phép chỉnh sửa nội dung. | `[readonly]="true"` |
334
+ | `showErrorLabel` | `boolean` | `true` | Hiển thị thông báo lỗi validation dạng text bên dưới editor. | `[showErrorLabel]="true"` |
335
+ | `showErrorBorder` | `boolean` | `false` | Hiển thị viền màu đỏ khi có lỗi validation. | `[showErrorBorder]="true"` |
336
+ | `onlyShowErrorBorderInContent` | `boolean` | `false` | Chỉ hiển thị viền đỏ ở vùng nội dung editor, không áp dụng cho cả container. | `[onlyShowErrorBorderInContent]="true"` |
337
+ | `classInclude` | `string` | `undefined` | CSS class bổ sung cho vùng nội dung `.ql-editor`. | `[classInclude]="'min-h-[200px]'"` |
338
+ | `classIncludeTemplate` | `string` | `undefined` | CSS class bổ sung cho container tổng thể của component. | `[classIncludeTemplate]="'border rounded'"` |
339
+ | `handlersExpand` | `Array<{ title: string; action: () => void }>` | `undefined` | Mảng các handler mở rộng cho toolbar. `title` là tên nút toolbar, `action` là hàm xử lý. | `[handlersExpand]="[{ title: 'link', action: myLinkHandler }]"` |
340
+ | `validRequired` | `IValidRequired` | `undefined` | Cấu hình validation bắt buộc nhập. | `[validRequired]="{ isRequired: true, message: 'Bắt buộc nhập' }"` |
341
+ | `validMinLength` | `IValidLength` | `undefined` | Cấu hình validation độ dài tối thiểu. | `[validMinLength]="{ length: 10, message: 'Tối thiểu 10 ký tự' }"` |
342
+ | `validMaxLength` | `IValidLength` | `undefined` | Cấu hình validation độ dài tối đa. | `[validMaxLength]="{ length: 5000, message: 'Tối đa 5000 ký tự' }"` |
343
+ | `zIndex` | `number` | `1250` | Z-index cho các overlay/popup (link editor, upload image). | `[zIndex]="2000"` |
344
+ | `dataConfigMention` | `IMentionConfig` | `undefined` | Cấu hình tính năng mention (@user). | `[dataConfigMention]="mentionConfig"` |
345
+ | `blotsRegister` | `Array<IQuillBlotRegister>` | `undefined` | Mảng các custom Quill blot để đăng ký thêm vào editor. | `[blotsRegister]="customBlots"` |
346
+ | `autoFocus` | `boolean` | `undefined` | Tự động focus vào editor khi component khởi tạo. | `[autoFocus]="true"` |
347
+ | `focusBottom` | `boolean` | `undefined` | Khi focus, đặt con trỏ ở cuối nội dung. | `[focusBottom]="true"` |
348
+ | `blockUndoRedoKeyboard` | `boolean` | `undefined` | Chặn phím tắt Ctrl+Z (Undo) và Ctrl+Shift+Z / Ctrl+Y (Redo). | `[blockUndoRedoKeyboard]="true"` |
349
+ | `templateToolBarPersonalize` | `TemplateRef` | `undefined` | Template tùy chỉnh hiển thị trong toolbar (slot "personalize"). | `[templateToolBarPersonalize]="myToolbarTpl"` |
350
+ | `template` | `TemplateRef` | `undefined` | Template tùy chỉnh hiển thị trong container của component. | `[template]="myCustomTpl"` |
351
+ | `uploadImageConfig` | `IQuillUploadImageConfig` | `uploadImageConfigDefault()` | Cấu hình tính năng upload hình ảnh. | `[uploadImageConfig]="uploadConfig"` |
352
+ | `heightAuto` | `boolean` | `undefined` | Cho phép editor tự động tăng chiều cao theo nội dung (không cố định scroll). | `[heightAuto]="true"` |
353
+ | `elementScrollHeightAuto` | `HTMLElement` | `undefined` | Element container dùng để scroll khi `heightAuto` được bật. | `[elementScrollHeightAuto]="scrollContainer"` |
354
+ | `ignoreShowPopupEditLink` | `boolean` | `undefined` | Khi `true`, không hiện popup mặc định khi edit link — thay vào đó emit `outShowPopupEditLink`. | `[ignoreShowPopupEditLink]="true"` |
355
+ | `ignoreCommunicateMicroEventPopup` | `boolean` | `undefined` | Bỏ qua việc giao tiếp sự kiện popup qua cơ chế micro-frontend. | `[ignoreCommunicateMicroEventPopup]="true"` |
356
+
357
+ ## @Output()
358
+
359
+ | Output | Type | Mô tả | Handler TS | Binding HTML |
360
+ |---|---|---|---|---|
361
+ | `(outChange)` | `string` | Emits chuỗi HTML khi nội dung editor thay đổi (sau xssFilter). | `handlerChange(html: string): void { event.stopPropagation?.(); this.content.set(html); }` | `(outChange)="handlerChange($event)"` |
362
+ | `(outFocus)` | `void` | Emits khi người dùng focus vào editor. | `handlerFocus(): void { this.isFocused.set(true); }` | `(outFocus)="handlerFocus()"` |
363
+ | `(outBlur)` | `void` | Emits khi editor mất focus. | `handlerBlur(): void { this.isFocused.set(false); }` | `(outBlur)="handlerBlur()"` |
364
+ | `(outFunctionsControl)` | `IQuillFunctionControlEvent` | Emits object chứa các hàm điều khiển editor từ bên ngoài. Emits ngay khi component khởi tạo. | `handlerFunctionsControl(ctrl: IQuillFunctionControlEvent): void { this.quillCtrl = ctrl; }` | `(outFunctionsControl)="handlerFunctionsControl($event)"` |
365
+ | `(outSelectionChange)` | `IQuillSelectionChange` | Emits khi vị trí con trỏ hoặc vùng chọn văn bản thay đổi. | `handlerSelectionChange(e: IQuillSelectionChange): void { console.log(e.range); }` | `(outSelectionChange)="handlerSelectionChange($event)"` |
366
+ | `(outTextChange)` | `IQuillTextChange` | Emits khi nội dung thay đổi, kèm delta diff chi tiết từ Quill. | `handlerTextChange(e: IQuillTextChange): void { console.log(e.delta); }` | `(outTextChange)="handlerTextChange($event)"` |
367
+ | `(outMessageError)` | `string` | Emits thông báo lỗi validation. Emits chuỗi rỗng khi hợp lệ. | `handlerMessageError(msg: string): void { this.errorMsg.set(msg); }` | `(outMessageError)="handlerMessageError($event)"` |
368
+ | `(outContextMenu)` | `MouseEvent` | Emits khi người dùng click chuột phải vào vùng editor. | `handlerContextMenu(e: MouseEvent): void { e.stopPropagation(); this.showContextMenu(e); }` | `(outContextMenu)="handlerContextMenu($event)"` |
369
+ | `(outShowPopupEditLink)` | `{ dataLink: IQuillLink; callback: (linkEdit: { title: string; link: string }) => Promise<void> }` | Emits khi cần hiện popup edit link tùy chỉnh. Chỉ emit khi `ignoreShowPopupEditLink` là `true`. | `handlerShowPopupEditLink(e: { dataLink: IQuillLink; callback: Function }): void { this.openCustomLinkDialog(e); }` | `(outShowPopupEditLink)="handlerShowPopupEditLink($event)"` |
370
+
371
+ ## FunctionsControl API (qua outFunctionsControl)
372
+
373
+ Khi nhận được object `IQuillFunctionControlEvent` từ `outFunctionsControl`, bạn có thể gọi các hàm sau:
374
+
375
+ | Method | Signature | Mô tả |
376
+ |---|---|---|
377
+ | `checkIsValid` | `() => Promise<boolean>` | Kiểm tra validation (validRequired, validMinLength, validMaxLength). Trả về `true` nếu hợp lệ. |
378
+ | `refreshItemValue` | `() => void` | Cập nhật thủ công `item[fieldNameBind]` từ nội dung hiện tại của editor. Dùng khi `autoUpdateValueWhenTextChange = false`. |
379
+ | `setContent` | `(content: string) => Promise<void>` | Đặt nội dung HTML cho editor (qua xssFilter). |
380
+ | `insertText` | `(value: string, index?: number, focusLast?: boolean) => Promise<void>` | Chèn văn bản vào vị trí con trỏ hiện tại hoặc tại `index`. |
381
+ | `insertLink` | `(value: string, url: string, index?: number) => Promise<void>` | Chèn hyperlink vào editor. |
382
+ | `insertImage` | `(content: string, index?: number) => Promise<void>` | Chèn hình ảnh (URL) vào editor. |
383
+ | `setFontSize` | `(size: number) => Promise<void>` | Đặt font size (px) tại vị trí con trỏ. |
384
+ | `setColor` | `(color: string) => Promise<void>` | Đặt màu chữ tại vị trí con trỏ (CSS color string). |
385
+ | `setBackground` | `(color: string) => Promise<void>` | Đặt màu nền tại vị trí con trỏ. |
386
+ | `quill` | `() => Quill` | Trả về instance Quill.js để truy cập trực tiếp các API của Quill. |
387
+ | `scrollToSelectionWithElementScrollHeightAuto` | `(index?: number) => void` | Scroll đến vị trí con trỏ khi dùng `heightAuto`. |
388
+ | `insertEmbed` | `(range: { index: number; indexSelect: number; length?: number }, type: string, data: unknown, sources?: Sources) => Promise<void>` | Chèn một embed blot tùy chỉnh. |
389
+ | `reCalculatorToolbar` | `() => Promise<void>` | Tính toán lại độ rộng toolbar (dùng khi container thay đổi kích thước). |
390
+
391
+ ## Types & Interfaces
392
+
393
+ ```typescript
394
+ import {
395
+ IQuillToolbarConfig,
396
+ IQuillUploadImageConfig,
397
+ IQuillFunctionControlEvent,
398
+ IQuillLink,
399
+ IQuillBlotRegister,
400
+ IQuillSelectionChange,
401
+ IQuillTextChange,
402
+ TYPE_MODE_BAR_CONFIG_OPTION,
403
+ } from '@libs-ui/components-inputs-quill';
404
+
405
+ // Chế độ toolbar
406
+ export type TYPE_MODE_BAR_CONFIG_OPTION = 'default' | 'basic' | 'custom';
407
+
408
+ // Cấu hình một item trong toolbar
409
+ export interface IQuillToolbarConfig {
410
+ type: string; // Loại nút (undo, redo, bold, italic, color, ...)
411
+ mode?: Array<TYPE_MODE_BAR_CONFIG_OPTION>; // Toolbar mode nào hiển thị nút này
412
+ width: number; // Độ rộng nút (px) dùng để tính layout toolbar
413
+ display?: boolean; // Hiện/ẩn nút (quản lý bởi toolbar calculator)
414
+ classInclude?: string; // CSS class bổ sung cho nút
415
+ }
416
+
417
+ // Cấu hình upload hình ảnh
418
+ export interface IQuillUploadImageConfig {
419
+ modeCustom: boolean; // true: hiển thị dialog tùy chỉnh; false: mở file picker mặc định
420
+ showIcon?: boolean; // Hiện icon upload trong toolbar
421
+ zIndex?: number; // Z-index của dialog upload
422
+ maxImageSize?: number; // Kích thước file tối đa (bytes), mặc định 5MB
423
+ label?: ILabel; // Label cho dialog upload
424
+ onlyAcceptImageHttpsLink?: boolean; // Chỉ chấp nhận link https (không chấp nhận base64)
425
+ functionUploadImage?: (files: File[]) => Promise<Array<string | ArrayBuffer>>; // Hàm gọi API upload, trả về array URL
426
+ }
427
+
428
+ // Interface điều khiển editor từ bên ngoài (nhận qua outFunctionsControl)
429
+ export interface IQuillFunctionControlEvent {
430
+ checkIsValid: () => Promise<boolean>;
431
+ refreshItemValue: () => void;
432
+ setContent: (content: string) => Promise<void>;
433
+ insertText: (value: string, index?: number, focusLast?: boolean) => Promise<void>;
434
+ insertLink: (value: string, url: string, index?: number) => Promise<void>;
435
+ insertImage: (content: string, index?: number) => Promise<void>;
436
+ setFontSize: (size: number) => Promise<void>;
437
+ setColor: (color: string) => Promise<void>;
438
+ setBackground: (color: string) => Promise<void>;
439
+ quill: () => Quill;
440
+ scrollToSelectionWithElementScrollHeightAuto: (index?: number) => void;
441
+ insertEmbed: (range: { index: number; indexSelect: number; length?: number }, type: string, data: unknown, sources?: Sources) => Promise<void>;
442
+ reCalculatorToolbar: () => Promise<void>;
443
+ }
444
+
445
+ // Thông tin một link trong editor
446
+ export interface IQuillLink {
447
+ title: string;
448
+ url: string;
449
+ range: {
450
+ index: number;
451
+ length: number;
452
+ };
453
+ }
454
+
455
+ // Đăng ký custom blot
456
+ export interface IQuillBlotRegister {
457
+ component: any; // Class blot kế thừa từ Quill Blot
458
+ className: string; // CSS class name của blot trong HTML output
459
+ style: string; // CSS style inline áp dụng cho blot
460
+ ignoreDelete?: boolean; // Khi true, ngăn không cho xóa blot bằng phím Backspace/Delete
461
+ }
462
+
463
+ // Dữ liệu sự kiện khi vùng chọn thay đổi
464
+ export interface IQuillSelectionChange {
465
+ quill: Quill;
466
+ range: RangeStatic;
467
+ oldRange: RangeStatic;
468
+ source: Sources;
469
+ }
470
+
471
+ // Dữ liệu sự kiện khi nội dung thay đổi
472
+ export interface IQuillTextChange {
473
+ quill: Quill;
474
+ delta: Delta;
475
+ }
476
+ ```
477
+
478
+ ## Utility Functions
479
+
480
+ ```typescript
481
+ import {
482
+ getHTMLFromDeltaOfQuill,
483
+ convertHtmlToDivBlocks,
484
+ getDeltaOfQuillFromHTML,
485
+ processPasteData,
486
+ insertContentWithRange,
487
+ } from '@libs-ui/components-inputs-quill';
488
+
489
+ // Chuyển đổi Quill Delta object sang chuỗi HTML
490
+ const html = getHTMLFromDeltaOfQuill(delta);
491
+
492
+ // Chuyển đổi HTML sang cấu trúc div blocks
493
+ const blocks = convertHtmlToDivBlocks(html);
494
+
495
+ // Chuyển đổi HTML sang Quill Delta object
496
+ const delta = getDeltaOfQuillFromHTML(html);
497
+
498
+ // Xử lý dữ liệu khi paste vào editor
499
+ const processedData = processPasteData(pasteEvent);
500
+
501
+ // Chèn nội dung vào editor tại vị trí range chỉ định
502
+ insertContentWithRange(quillInstance, content, range);
503
+ ```
504
+
505
+ ## Lưu ý quan trọng
506
+
507
+ ⚠️ **autoUpdateValueWhenTextChange = false để tối ưu hiệu năng**: Khi editor có nội dung lớn hoặc nhiều hình ảnh, việc update `item` mỗi lần gõ phím có thể ảnh hưởng hiệu năng. Đặt `[autoUpdateValueWhenTextChange]="false"` và chủ động gọi `functionsControl.refreshItemValue()` khi cần lấy giá trị (ví dụ: trước khi submit form).
508
+
509
+ ⚠️ **Toolbar tự động ẩn nút thừa**: Toolbar sử dụng cơ chế tính toán độ rộng để ẩn các nút không vừa vào container vào menu "Xem thêm". Nếu cần trigger lại sau khi container thay đổi kích thước, gọi `functionsControl.reCalculatorToolbar()`.
510
+
511
+ ⚠️ **XSS Security**: Tất cả nội dung đặt vào editor (qua `setContent`) đều được xử lý qua `xssFilter` trước khi render. Nội dung đầu ra (`outChange`) cũng đã được lọc. Không cần sanitize thêm ở phía consumer.
512
+
513
+ ⚠️ **Custom blots và ignoreDelete**: Khi đăng ký custom blot với `ignoreDelete: true`, phím Backspace/Delete sẽ không xóa blot đó. Dùng khi blot là nội dung không thể xóa trực tiếp (ví dụ: mention node, reflection node).
514
+
515
+ ⚠️ **Upload hình ảnh**: `functionUploadImage` trong `IQuillUploadImageConfig` phải trả về `Promise<Array<string | ArrayBuffer>>`. Khi `onlyAcceptImageHttpsLink = true`, các link không phải `https://` hoặc base64 sẽ bị bỏ qua. Kích thước file vượt quá `maxImageSize` sẽ hiện thông báo cảnh báo.
516
+
517
+ ⚠️ **Peer dependencies**: Component phụ thuộc vào `quill@^1.x` và các thư viện nội bộ `@libs-ui`. Đảm bảo đã cài đặt đầy đủ peer dependencies trước khi sử dụng.
518
+
519
+ ⚠️ **outFunctionsControl emits ngay khi khởi tạo**: Output `outFunctionsControl` sẽ emit object `IQuillFunctionControlEvent` ngay trong `ngOnInit`. Hàm `setContent` và các API khác chỉ hoạt động sau khi `ngAfterViewInit` hoàn tất (có delay ~20ms). Nếu cần gọi `setContent` ngay sau khi nhận control, có thể dùng `setTimeout` ngắn.
520
+
521
+ ## Demo
522
+
523
+ ```bash
524
+ npx nx serve core-ui
525
+ ```
526
+
527
+ Truy cập: http://localhost:4500/components/quill
@@ -1,6 +1,6 @@
1
1
  import { NgTemplateOutlet } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { signal, input, output, ChangeDetectionStrategy, Component, computed, inject, model, viewChild, ChangeDetectorRef, effect, untracked } from '@angular/core';
3
+ import { signal, input, output, Component, ChangeDetectionStrategy, computed, inject, model, viewChild, ChangeDetectorRef, effect, untracked } from '@angular/core';
4
4
  import { LibsUiComponentsButtonsButtonComponent } from '@libs-ui/components-buttons-button';
5
5
  import { LibsUiComponentsButtonsSelectColorComponent } from '@libs-ui/components-buttons-select-color';
6
6
  import { LibsUiComponentsDropdownComponent } from '@libs-ui/components-dropdown';