@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
|
-
|
|
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,
|
|
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';
|