@libs-ui/components-inputs-quill 0.2.356-9 → 0.2.357-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,545 @@
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
+ > Các package phụ thuộc (`quill@1.3.7`, `parchment`, `quill-delta`...) đã khai báo `peerDependencies` trong `package.json` của lib → trình quản lý gói tự cài kèm, không cần cài thủ công.
40
+
41
+ ### ⚠️ Nạp CSS của Quill (BẮT BUỘC — nếu thiếu, editor/toolbar sẽ vỡ giao diện)
42
+
43
+ Quill ship style riêng, **KHÔNG** đi kèm trong bundle của component. Consumer PHẢI tự nạp CSS theme. Khai báo trong `angular.json` / `project.json`:
44
+
45
+ ```jsonc
46
+ // architect.build.options.styles (Angular CLI) HOẶC targets.build.options.styles (Nx)
47
+ "styles": [
48
+ "src/styles.scss",
49
+ "./node_modules/quill/dist/quill.snow.css",
50
+ "./node_modules/quill/dist/quill.bubble.css"
51
+ ]
52
+ ```
53
+
54
+ > 🔴 `quill.snow.css` là theme mặc định (BẮT BUỘC). Chỉ thêm `quill.bubble.css` nếu dùng theme bubble.
55
+ > Khai báo `allowedCommonJsDependencies: ["quill"]` trong cấu hình build để tắt cảnh báo CommonJS.
56
+
57
+ ## Import
58
+
59
+ ```typescript
60
+ import { LibsUiComponentsInputsQuillComponent } from '@libs-ui/components-inputs-quill';
61
+
62
+ // Các interface và types
63
+ import {
64
+ IQuillFunctionControlEvent,
65
+ IQuillUploadImageConfig,
66
+ IQuillBlotRegister,
67
+ IQuillLink,
68
+ IQuillSelectionChange,
69
+ IQuillTextChange,
70
+ IQuillToolbarConfig,
71
+ TYPE_MODE_BAR_CONFIG_OPTION,
72
+ } from '@libs-ui/components-inputs-quill';
73
+
74
+ // Các utility functions
75
+ import {
76
+ getHTMLFromDeltaOfQuill,
77
+ convertHtmlToDivBlocks,
78
+ getDeltaOfQuillFromHTML,
79
+ processPasteData,
80
+ insertContentWithRange,
81
+ } from '@libs-ui/components-inputs-quill';
82
+ ```
83
+
84
+ ## Ví dụ sử dụng
85
+
86
+ ### 1. Cơ bản — Two-way binding
87
+
88
+ ```typescript
89
+ // component.ts
90
+ import { Component, signal } from '@angular/core';
91
+ import { LibsUiComponentsInputsQuillComponent } from '@libs-ui/components-inputs-quill';
92
+
93
+ @Component({
94
+ standalone: true,
95
+ changeDetection: ChangeDetectionStrategy.OnPush,
96
+ imports: [LibsUiComponentsInputsQuillComponent],
97
+ template: `
98
+ <libs_ui-components-inputs-quill
99
+ [(item)]="formData"
100
+ [fieldNameBind]="'content'"
101
+ [placeholder]="'Nhập nội dung tại đây...'"
102
+ ></libs_ui-components-inputs-quill>
103
+ `,
104
+ })
105
+ export class MyComponent {
106
+ protected formData = signal<Record<string, string>>({ content: '' });
107
+ }
108
+ ```
109
+
110
+ ### 2. Với Validation và Label
111
+
112
+ ```typescript
113
+ // component.ts
114
+ import { Component, signal } from '@angular/core';
115
+ import { LibsUiComponentsInputsQuillComponent } from '@libs-ui/components-inputs-quill';
116
+ import { IValidRequired, IValidLength } from '@libs-ui/components-inputs-valid';
117
+
118
+ @Component({
119
+ standalone: true,
120
+ changeDetection: ChangeDetectionStrategy.OnPush,
121
+ imports: [LibsUiComponentsInputsQuillComponent],
122
+ templateUrl: './my.component.html',
123
+ })
124
+ export class MyComponent {
125
+ protected formData = signal<Record<string, string>>({ description: '' });
126
+
127
+ protected validRequired: IValidRequired = {
128
+ isRequired: true,
129
+ message: 'Vui lòng nhập mô tả',
130
+ };
131
+
132
+ protected validMinLength: IValidLength = {
133
+ length: 10,
134
+ message: 'Mô tả phải có ít nhất 10 ký tự',
135
+ };
136
+
137
+ protected validMaxLength: IValidLength = {
138
+ length: 5000,
139
+ message: 'Mô tả không được vượt quá 5000 ký tự',
140
+ };
141
+ }
142
+ ```
143
+
144
+ ```html
145
+ <!-- my.component.html -->
146
+ <libs_ui-components-inputs-quill
147
+ [(item)]="formData"
148
+ [fieldNameBind]="'description'"
149
+ [label]="{ title: 'Mô tả chi tiết', isRequired: true }"
150
+ [validRequired]="validRequired"
151
+ [validMinLength]="validMinLength"
152
+ [validMaxLength]="validMaxLength"
153
+ [showErrorLabel]="true"
154
+ [showErrorBorder]="true"
155
+ ></libs_ui-components-inputs-quill>
156
+ ```
157
+
158
+ ### 3. Với Mention và Upload hình ảnh
159
+
160
+ ```typescript
161
+ // component.ts
162
+ import { Component, signal } from '@angular/core';
163
+ import { LibsUiComponentsInputsQuillComponent, IQuillUploadImageConfig } from '@libs-ui/components-inputs-quill';
164
+ import { IMentionConfig } from '@libs-ui/components-inputs-mention';
165
+
166
+ @Component({
167
+ standalone: true,
168
+ changeDetection: ChangeDetectionStrategy.OnPush,
169
+ imports: [LibsUiComponentsInputsQuillComponent],
170
+ templateUrl: './my.component.html',
171
+ })
172
+ export class MyComponent {
173
+ protected formData = signal<Record<string, string>>({ content: '' });
174
+
175
+ protected mentionConfig: IMentionConfig = {
176
+ items: [
177
+ { id: '1', name: 'Nguyễn Văn An', username: 'an@example.com' },
178
+ { id: '2', name: 'Trần Thị Bình', username: 'binh@example.com' },
179
+ { id: '3', name: 'Lê Văn Cường', username: 'cuong@example.com' },
180
+ ],
181
+ triggerChar: '@',
182
+ labelKey: 'name',
183
+ mentionFilter: (search, items) =>
184
+ items?.filter((item) => item.name.toLowerCase().includes(search.toLowerCase())),
185
+ mentionSelect: (item, triggerChar) => `${triggerChar || '@'}${item.name}`,
186
+ };
187
+
188
+ protected uploadImageConfig: IQuillUploadImageConfig = {
189
+ modeCustom: false,
190
+ showIcon: true,
191
+ maxImageSize: 5 * 1024 * 1024, // 5MB
192
+ onlyAcceptImageHttpsLink: false,
193
+ functionUploadImage: async (files: File[]) => {
194
+ // Gọi API upload file và trả về array URL
195
+ const urls: string[] = [];
196
+ for (const file of files) {
197
+ const url = await this.uploadToServer(file);
198
+ urls.push(url);
199
+ }
200
+ return urls;
201
+ },
202
+ };
203
+
204
+ private async uploadToServer(file: File): Promise<string> {
205
+ // Implement API call thực tế
206
+ return 'https://example.com/images/uploaded.jpg';
207
+ }
208
+ }
209
+ ```
210
+
211
+ ```html
212
+ <!-- my.component.html -->
213
+ <libs_ui-components-inputs-quill
214
+ [(item)]="formData"
215
+ [fieldNameBind]="'content'"
216
+ [dataConfigMention]="mentionConfig"
217
+ [uploadImageConfig]="uploadImageConfig"
218
+ [placeholder]="'Gõ @ để mention người dùng...'"
219
+ ></libs_ui-components-inputs-quill>
220
+ ```
221
+
222
+ ### 4. Lấy API điều khiển qua outFunctionsControl
223
+
224
+ ```typescript
225
+ // component.ts
226
+ import { Component, signal } from '@angular/core';
227
+ import { LibsUiComponentsInputsQuillComponent, IQuillFunctionControlEvent } from '@libs-ui/components-inputs-quill';
228
+
229
+ @Component({
230
+ standalone: true,
231
+ changeDetection: ChangeDetectionStrategy.OnPush,
232
+ imports: [LibsUiComponentsInputsQuillComponent],
233
+ templateUrl: './my.component.html',
234
+ })
235
+ export class MyComponent {
236
+ protected formData = signal<Record<string, string>>({ content: '' });
237
+
238
+ private quillControl: IQuillFunctionControlEvent | undefined;
239
+
240
+ protected handlerFunctionsControl(event: IQuillFunctionControlEvent): void {
241
+ event.stopPropagation?.();
242
+ this.quillControl = event;
243
+ }
244
+
245
+ protected async handlerSetContent(event: Event): Promise<void> {
246
+ event.stopPropagation();
247
+ await this.quillControl?.setContent('<p>Nội dung mới được đặt từ bên ngoài</p>');
248
+ }
249
+
250
+ protected async handlerInsertText(event: Event): Promise<void> {
251
+ event.stopPropagation();
252
+ await this.quillControl?.insertText('Văn bản được chèn');
253
+ }
254
+
255
+ protected async handlerValidate(event: Event): Promise<void> {
256
+ event.stopPropagation();
257
+ const isValid = await this.quillControl?.checkIsValid();
258
+ console.log('Is valid:', isValid);
259
+ }
260
+
261
+ protected async handlerInsertLink(event: Event): Promise<void> {
262
+ event.stopPropagation();
263
+ await this.quillControl?.insertLink('Xem chi tiết', 'https://example.com');
264
+ }
265
+ }
266
+ ```
267
+
268
+ ```html
269
+ <!-- my.component.html -->
270
+ <libs_ui-components-inputs-quill
271
+ [(item)]="formData"
272
+ [fieldNameBind]="'content'"
273
+ (outFunctionsControl)="handlerFunctionsControl($event)"
274
+ ></libs_ui-components-inputs-quill>
275
+
276
+ <button (click)="handlerSetContent($event)">Đặt nội dung</button>
277
+ <button (click)="handlerInsertText($event)">Chèn văn bản</button>
278
+ <button (click)="handlerInsertLink($event)">Chèn link</button>
279
+ <button (click)="handlerValidate($event)">Kiểm tra hợp lệ</button>
280
+ ```
281
+
282
+ ### 5. Chế độ toolbar tùy chỉnh
283
+
284
+ ```typescript
285
+ // component.ts
286
+ import { Component, signal } from '@angular/core';
287
+ import { LibsUiComponentsInputsQuillComponent, IQuillToolbarConfig, TYPE_MODE_BAR_CONFIG_OPTION } from '@libs-ui/components-inputs-quill';
288
+
289
+ @Component({
290
+ standalone: true,
291
+ changeDetection: ChangeDetectionStrategy.OnPush,
292
+ imports: [LibsUiComponentsInputsQuillComponent],
293
+ templateUrl: './my.component.html',
294
+ })
295
+ export class MyComponent {
296
+ protected formData = signal<Record<string, string>>({ content: '' });
297
+
298
+ protected toolbarConfig: { type: TYPE_MODE_BAR_CONFIG_OPTION; config?: IQuillToolbarConfig[] } = {
299
+ type: 'basic',
300
+ };
301
+ }
302
+ ```
303
+
304
+ ```html
305
+ <!-- my.component.html -->
306
+ <libs_ui-components-inputs-quill
307
+ [(item)]="formData"
308
+ [fieldNameBind]="'content'"
309
+ [toolbarConfig]="toolbarConfig"
310
+ [isShowToolBar]="true"
311
+ ></libs_ui-components-inputs-quill>
312
+ ```
313
+
314
+ ### 6. Readonly mode
315
+
316
+ ```html
317
+ <libs_ui-components-inputs-quill
318
+ [(item)]="formData"
319
+ [fieldNameBind]="'content'"
320
+ [readonly]="true"
321
+ [isShowToolBar]="false"
322
+ ></libs_ui-components-inputs-quill>
323
+ ```
324
+
325
+ ### 7. Auto focus và heightAuto
326
+
327
+ ```html
328
+ <libs_ui-components-inputs-quill
329
+ [(item)]="formData"
330
+ [fieldNameBind]="'content'"
331
+ [autoFocus]="true"
332
+ [focusBottom]="true"
333
+ [heightAuto]="true"
334
+ ></libs_ui-components-inputs-quill>
335
+ ```
336
+
337
+ ## @Input()
338
+
339
+ | Input | Type | Default | Mô tả | Ví dụ |
340
+ |---|---|---|---|---|
341
+ | `item` | `TYPE_OBJECT` (model) | `undefined` | Object chứa dữ liệu two-way binding. Dùng `[(item)]` để binding hai chiều. | `[(item)]="formData"` |
342
+ | `fieldNameBind` | `string` | `'value'` | Tên trường trong `item` dùng để đọc/ghi nội dung HTML của editor. | `[fieldNameBind]="'content'"` |
343
+ | `isShowToolBar` | `boolean` | `true` | Hiển thị hoặc ẩn thanh toolbar. | `[isShowToolBar]="false"` |
344
+ | `isToolbarPositionFixed` | `boolean` | `false` | Đặt toolbar ở vị trí cố định (fixed) thay vì inline. | `[isToolbarPositionFixed]="true"` |
345
+ | `classIncludeToolbar` | `string` | `''` | CSS class bổ sung cho container toolbar. | `[classIncludeToolbar]="'border-b'"` |
346
+ | `stylesIncludeToolbar` | `TYPE_OBJECT` | `undefined` | Style object bổ sung cho toolbar. | `[stylesIncludeToolbar]="{ background: '#f5f5f5' }"` |
347
+ | `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' }"` |
348
+ | `placeholder` | `string` | `'i18n_import_content'` | Placeholder text khi editor trống. | `[placeholder]="'Nhập nội dung...'"` |
349
+ | `label` | `ILabel` | `undefined` | Cấu hình label hiển thị bên trên editor. | `[label]="{ title: 'Nội dung', isRequired: true }"` |
350
+ | `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"` |
351
+ | `readonly` | `boolean` | `undefined` | Chế độ chỉ đọc, không cho phép chỉnh sửa nội dung. | `[readonly]="true"` |
352
+ | `showErrorLabel` | `boolean` | `true` | Hiển thị thông báo lỗi validation dạng text bên dưới editor. | `[showErrorLabel]="true"` |
353
+ | `showErrorBorder` | `boolean` | `false` | Hiển thị viền màu đỏ khi có lỗi validation. | `[showErrorBorder]="true"` |
354
+ | `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"` |
355
+ | `classInclude` | `string` | `undefined` | CSS class bổ sung cho vùng nội dung `.ql-editor`. | `[classInclude]="'min-h-[200px]'"` |
356
+ | `classIncludeTemplate` | `string` | `undefined` | CSS class bổ sung cho container tổng thể của component. | `[classIncludeTemplate]="'border rounded'"` |
357
+ | `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 }]"` |
358
+ | `validRequired` | `IValidRequired` | `undefined` | Cấu hình validation bắt buộc nhập. | `[validRequired]="{ isRequired: true, message: 'Bắt buộc nhập' }"` |
359
+ | `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ự' }"` |
360
+ | `validMaxLength` | `IValidLength` | `undefined` | Cấu hình validation độ dài tối đa. | `[validMaxLength]="{ length: 5000, message: 'Tối đa 5000 ký tự' }"` |
361
+ | `zIndex` | `number` | `1250` | Z-index cho các overlay/popup (link editor, upload image). | `[zIndex]="2000"` |
362
+ | `dataConfigMention` | `IMentionConfig` | `undefined` | Cấu hình tính năng mention (@user). | `[dataConfigMention]="mentionConfig"` |
363
+ | `blotsRegister` | `Array<IQuillBlotRegister>` | `undefined` | Mảng các custom Quill blot để đăng ký thêm vào editor. | `[blotsRegister]="customBlots"` |
364
+ | `autoFocus` | `boolean` | `undefined` | Tự động focus vào editor khi component khởi tạo. | `[autoFocus]="true"` |
365
+ | `focusBottom` | `boolean` | `undefined` | Khi focus, đặt con trỏ ở cuối nội dung. | `[focusBottom]="true"` |
366
+ | `blockUndoRedoKeyboard` | `boolean` | `undefined` | Chặn phím tắt Ctrl+Z (Undo) và Ctrl+Shift+Z / Ctrl+Y (Redo). | `[blockUndoRedoKeyboard]="true"` |
367
+ | `templateToolBarPersonalize` | `TemplateRef` | `undefined` | Template tùy chỉnh hiển thị trong toolbar (slot "personalize"). | `[templateToolBarPersonalize]="myToolbarTpl"` |
368
+ | `template` | `TemplateRef` | `undefined` | Template tùy chỉnh hiển thị trong container của component. | `[template]="myCustomTpl"` |
369
+ | `uploadImageConfig` | `IQuillUploadImageConfig` | `uploadImageConfigDefault()` | Cấu hình tính năng upload hình ảnh. | `[uploadImageConfig]="uploadConfig"` |
370
+ | `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"` |
371
+ | `elementScrollHeightAuto` | `HTMLElement` | `undefined` | Element container dùng để scroll khi `heightAuto` được bật. | `[elementScrollHeightAuto]="scrollContainer"` |
372
+ | `ignoreShowPopupEditLink` | `boolean` | `undefined` | Khi `true`, không hiện popup mặc định khi edit link — thay vào đó emit `outShowPopupEditLink`. | `[ignoreShowPopupEditLink]="true"` |
373
+ | `ignoreCommunicateMicroEventPopup` | `boolean` | `undefined` | Bỏ qua việc giao tiếp sự kiện popup qua cơ chế micro-frontend. | `[ignoreCommunicateMicroEventPopup]="true"` |
374
+
375
+ ## @Output()
376
+
377
+ | Output | Type | Mô tả | Handler TS | Binding HTML |
378
+ |---|---|---|---|---|
379
+ | `(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)"` |
380
+ | `(outFocus)` | `void` | Emits khi người dùng focus vào editor. | `handlerFocus(): void { this.isFocused.set(true); }` | `(outFocus)="handlerFocus()"` |
381
+ | `(outBlur)` | `void` | Emits khi editor mất focus. | `handlerBlur(): void { this.isFocused.set(false); }` | `(outBlur)="handlerBlur()"` |
382
+ | `(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)"` |
383
+ | `(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)"` |
384
+ | `(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)"` |
385
+ | `(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)"` |
386
+ | `(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)"` |
387
+ | `(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)"` |
388
+
389
+ ## FunctionsControl API (qua outFunctionsControl)
390
+
391
+ Khi nhận được object `IQuillFunctionControlEvent` từ `outFunctionsControl`, bạn có thể gọi các hàm sau:
392
+
393
+ | Method | Signature | Mô tả |
394
+ |---|---|---|
395
+ | `checkIsValid` | `() => Promise<boolean>` | Kiểm tra validation (validRequired, validMinLength, validMaxLength). Trả về `true` nếu hợp lệ. |
396
+ | `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`. |
397
+ | `setContent` | `(content: string) => Promise<void>` | Đặt nội dung HTML cho editor (qua xssFilter). |
398
+ | `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`. |
399
+ | `insertLink` | `(value: string, url: string, index?: number) => Promise<void>` | Chèn hyperlink vào editor. |
400
+ | `insertImage` | `(content: string, index?: number) => Promise<void>` | Chèn hình ảnh (URL) vào editor. |
401
+ | `setFontSize` | `(size: number) => Promise<void>` | Đặt font size (px) tại vị trí con trỏ. |
402
+ | `setColor` | `(color: string) => Promise<void>` | Đặt màu chữ tại vị trí con trỏ (CSS color string). |
403
+ | `setBackground` | `(color: string) => Promise<void>` | Đặt màu nền tại vị trí con trỏ. |
404
+ | `quill` | `() => Quill` | Trả về instance Quill.js để truy cập trực tiếp các API của Quill. |
405
+ | `scrollToSelectionWithElementScrollHeightAuto` | `(index?: number) => void` | Scroll đến vị trí con trỏ khi dùng `heightAuto`. |
406
+ | `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. |
407
+ | `reCalculatorToolbar` | `() => Promise<void>` | Tính toán lại độ rộng toolbar (dùng khi container thay đổi kích thước). |
408
+
409
+ ## Types & Interfaces
410
+
411
+ ```typescript
412
+ import {
413
+ IQuillToolbarConfig,
414
+ IQuillUploadImageConfig,
415
+ IQuillFunctionControlEvent,
416
+ IQuillLink,
417
+ IQuillBlotRegister,
418
+ IQuillSelectionChange,
419
+ IQuillTextChange,
420
+ TYPE_MODE_BAR_CONFIG_OPTION,
421
+ } from '@libs-ui/components-inputs-quill';
422
+
423
+ // Chế độ toolbar
424
+ export type TYPE_MODE_BAR_CONFIG_OPTION = 'default' | 'basic' | 'custom';
425
+
426
+ // Cấu hình một item trong toolbar
427
+ export interface IQuillToolbarConfig {
428
+ type: string; // Loại nút (undo, redo, bold, italic, color, ...)
429
+ mode?: Array<TYPE_MODE_BAR_CONFIG_OPTION>; // Toolbar mode nào hiển thị nút này
430
+ width: number; // Độ rộng nút (px) dùng để tính layout toolbar
431
+ display?: boolean; // Hiện/ẩn nút (quản lý bởi toolbar calculator)
432
+ classInclude?: string; // CSS class bổ sung cho nút
433
+ }
434
+
435
+ // Cấu hình upload hình ảnh
436
+ export interface IQuillUploadImageConfig {
437
+ modeCustom: boolean; // true: hiển thị dialog tùy chỉnh; false: mở file picker mặc định
438
+ showIcon?: boolean; // Hiện icon upload trong toolbar
439
+ zIndex?: number; // Z-index của dialog upload
440
+ maxImageSize?: number; // Kích thước file tối đa (bytes), mặc định 5MB
441
+ label?: ILabel; // Label cho dialog upload
442
+ onlyAcceptImageHttpsLink?: boolean; // Chỉ chấp nhận link https (không chấp nhận base64)
443
+ functionUploadImage?: (files: File[]) => Promise<Array<string | ArrayBuffer>>; // Hàm gọi API upload, trả về array URL
444
+ }
445
+
446
+ // Interface điều khiển editor từ bên ngoài (nhận qua outFunctionsControl)
447
+ export interface IQuillFunctionControlEvent {
448
+ checkIsValid: () => Promise<boolean>;
449
+ refreshItemValue: () => void;
450
+ setContent: (content: string) => Promise<void>;
451
+ insertText: (value: string, index?: number, focusLast?: boolean) => Promise<void>;
452
+ insertLink: (value: string, url: string, index?: number) => Promise<void>;
453
+ insertImage: (content: string, index?: number) => Promise<void>;
454
+ setFontSize: (size: number) => Promise<void>;
455
+ setColor: (color: string) => Promise<void>;
456
+ setBackground: (color: string) => Promise<void>;
457
+ quill: () => Quill;
458
+ scrollToSelectionWithElementScrollHeightAuto: (index?: number) => void;
459
+ insertEmbed: (range: { index: number; indexSelect: number; length?: number }, type: string, data: unknown, sources?: Sources) => Promise<void>;
460
+ reCalculatorToolbar: () => Promise<void>;
461
+ }
462
+
463
+ // Thông tin một link trong editor
464
+ export interface IQuillLink {
465
+ title: string;
466
+ url: string;
467
+ range: {
468
+ index: number;
469
+ length: number;
470
+ };
471
+ }
472
+
473
+ // Đăng ký custom blot
474
+ export interface IQuillBlotRegister {
475
+ component: any; // Class blot kế thừa từ Quill Blot
476
+ className: string; // CSS class name của blot trong HTML output
477
+ style: string; // CSS style inline áp dụng cho blot
478
+ ignoreDelete?: boolean; // Khi true, ngăn không cho xóa blot bằng phím Backspace/Delete
479
+ }
480
+
481
+ // Dữ liệu sự kiện khi vùng chọn thay đổi
482
+ export interface IQuillSelectionChange {
483
+ quill: Quill;
484
+ range: RangeStatic;
485
+ oldRange: RangeStatic;
486
+ source: Sources;
487
+ }
488
+
489
+ // Dữ liệu sự kiện khi nội dung thay đổi
490
+ export interface IQuillTextChange {
491
+ quill: Quill;
492
+ delta: Delta;
493
+ }
494
+ ```
495
+
496
+ ## Utility Functions
497
+
498
+ ```typescript
499
+ import {
500
+ getHTMLFromDeltaOfQuill,
501
+ convertHtmlToDivBlocks,
502
+ getDeltaOfQuillFromHTML,
503
+ processPasteData,
504
+ insertContentWithRange,
505
+ } from '@libs-ui/components-inputs-quill';
506
+
507
+ // Chuyển đổi Quill Delta object sang chuỗi HTML
508
+ const html = getHTMLFromDeltaOfQuill(delta);
509
+
510
+ // Chuyển đổi HTML sang cấu trúc div blocks
511
+ const blocks = convertHtmlToDivBlocks(html);
512
+
513
+ // Chuyển đổi HTML sang Quill Delta object
514
+ const delta = getDeltaOfQuillFromHTML(html);
515
+
516
+ // Xử lý dữ liệu khi paste vào editor
517
+ const processedData = processPasteData(pasteEvent);
518
+
519
+ // Chèn nội dung vào editor tại vị trí range chỉ định
520
+ insertContentWithRange(quillInstance, content, range);
521
+ ```
522
+
523
+ ## Lưu ý quan trọng
524
+
525
+ ⚠️ **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).
526
+
527
+ ⚠️ **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()`.
528
+
529
+ ⚠️ **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.
530
+
531
+ ⚠️ **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).
532
+
533
+ ⚠️ **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.
534
+
535
+ ⚠️ **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.
536
+
537
+ ⚠️ **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.
538
+
539
+ ## Demo
540
+
541
+ ```bash
542
+ npx nx serve core-ui
543
+ ```
544
+
545
+ Truy cập: http://localhost:4500/components/quill
@@ -1,5 +1,5 @@
1
1
  declare const Block: any;
2
2
  export declare class QuillDivBlot extends Block {
3
- static tagName: string;
3
+ static readonly tagName = "div";
4
4
  }
5
5
  export {};
@@ -1,8 +1,8 @@
1
1
  declare const Embed: any;
2
2
  export declare class QuillMentionBlot extends Embed {
3
- static blotName: string;
4
- static tagName: string;
5
- static className: string;
3
+ static readonly blotName = "mention";
4
+ static readonly tagName = "span";
5
+ static readonly className = "libs-ui-quill-mention";
6
6
  static create(data: {
7
7
  id: string;
8
8
  feId: string;
@@ -3,4 +3,4 @@ const Block = Quill.import('blots/block');
3
3
  export class QuillDivBlot extends Block {
4
4
  static tagName = 'div';
5
5
  }
6
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGl2LmNvbXBvbmVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uLy4uL2xpYnMtdWkvY29tcG9uZW50cy9pbnB1dHMvcXVpbGwvc3JjL2Jsb3RzLWN1c3RvbS9kaXYuY29tcG9uZW50LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxNQUFNLE9BQU8sQ0FBQztBQUUxQixNQUFNLEtBQUssR0FBRyxLQUFLLENBQUMsTUFBTSxDQUFDLGFBQWEsQ0FBQyxDQUFDO0FBRTFDLE1BQU0sT0FBTyxZQUFhLFNBQVEsS0FBSztJQUNyQyxNQUFNLENBQUMsT0FBTyxHQUFHLEtBQUssQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBRdWlsbCBmcm9tICdxdWlsbCc7XG5cbmNvbnN0IEJsb2NrID0gUXVpbGwuaW1wb3J0KCdibG90cy9ibG9jaycpO1xuXG5leHBvcnQgY2xhc3MgUXVpbGxEaXZCbG90IGV4dGVuZHMgQmxvY2sge1xuICBzdGF0aWMgdGFnTmFtZSA9ICdkaXYnO1xufVxuIl19
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGl2LmNvbXBvbmVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uLy4uL2xpYnMtdWkvY29tcG9uZW50cy9pbnB1dHMvcXVpbGwvc3JjL2Jsb3RzLWN1c3RvbS9kaXYuY29tcG9uZW50LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxNQUFNLE9BQU8sQ0FBQztBQUUxQixNQUFNLEtBQUssR0FBRyxLQUFLLENBQUMsTUFBTSxDQUFDLGFBQWEsQ0FBQyxDQUFDO0FBRTFDLE1BQU0sT0FBTyxZQUFhLFNBQVEsS0FBSztJQUNyQyxNQUFNLENBQVUsT0FBTyxHQUFHLEtBQUssQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBRdWlsbCBmcm9tICdxdWlsbCc7XG5cbmNvbnN0IEJsb2NrID0gUXVpbGwuaW1wb3J0KCdibG90cy9ibG9jaycpO1xuXG5leHBvcnQgY2xhc3MgUXVpbGxEaXZCbG90IGV4dGVuZHMgQmxvY2sge1xuICBzdGF0aWMgcmVhZG9ubHkgdGFnTmFtZSA9ICdkaXYnO1xufVxuIl19
@@ -4,7 +4,7 @@ const pixelLevels = [1, 2, 3, 4, 5, 6, 7, 8];
4
4
  const tabMultiplier = 3;
5
5
  class IndentAttributor extends parchment.Attributor.Style {
6
6
  constructor(formatName, styleProperty, attributorOptions) {
7
- super(formatName, styleProperty, attributorOptions);
7
+ super(formatName, styleProperty, attributorOptions); // NOSONAR — dynamic Quill Parchment class cast as constructor, intentional TypeScript pattern
8
8
  }
9
9
  add(node, value) {
10
10
  return super.add(node, `${+value * tabMultiplier}em`);
@@ -17,4 +17,4 @@ export const CustomIndentStyleBlot = new IndentAttributor('indent', 'margin-left
17
17
  scope: parchment.Scope.BLOCK,
18
18
  whitelist: pixelLevels.map((value) => `${value * tabMultiplier}em`),
19
19
  });
20
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZW50LWF0dHJpYnV0b3IuY29tcG9uZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vLi4vLi4vbGlicy11aS9jb21wb25lbnRzL2lucHV0cy9xdWlsbC9zcmMvYmxvdHMtY3VzdG9tL2luZGVudC1hdHRyaWJ1dG9yLmNvbXBvbmVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssTUFBTSxPQUFPLENBQUM7QUFFMUIsTUFBTSxTQUFTLEdBQUcsS0FBSyxDQUFDLE1BQU0sQ0FBQyxXQUFXLENBQUMsQ0FBQztBQUM1QyxNQUFNLFdBQVcsR0FBRyxDQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQztBQUM3QyxNQUFNLGFBQWEsR0FBRyxDQUFDLENBQUM7QUFFeEIsTUFBTSxnQkFBaUIsU0FBUyxTQUFTLENBQUMsVUFBVSxDQUFDLEtBR25EO0lBQ0EsWUFBWSxVQUFrQixFQUFFLGFBQXFCLEVBQUUsaUJBQTBCO1FBQy9FLEtBQUssQ0FBQyxVQUFVLEVBQUUsYUFBYSxFQUFFLGlCQUFpQixDQUFDLENBQUM7SUFDdEQsQ0FBQztJQUVNLEdBQUcsQ0FBQyxJQUFpQixFQUFFLEtBQWE7UUFDekMsT0FBTyxLQUFLLENBQUMsR0FBRyxDQUFDLElBQUksRUFBRSxHQUFHLENBQUMsS0FBSyxHQUFHLGFBQWEsSUFBSSxDQUFDLENBQUM7SUFDeEQsQ0FBQztJQUVNLEtBQUssQ0FBQyxJQUFpQjtRQUM1QixPQUFPLFVBQVUsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsYUFBYSxJQUFJLFNBQVMsQ0FBQyxDQUFDLG1CQUFtQjtJQUN4RixDQUFDO0NBQ0Y7QUFFRCxNQUFNLENBQUMsTUFBTSxxQkFBcUIsR0FBRyxJQUFJLGdCQUFnQixDQUFDLFFBQVEsRUFBRSxhQUFhLEVBQUU7SUFDakYsS0FBSyxFQUFFLFNBQVMsQ0FBQyxLQUFLLENBQUMsS0FBSztJQUM1QixTQUFTLEVBQUUsV0FBVyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsR0FBRyxLQUFLLEdBQUcsYUFBYSxJQUFJLENBQUM7Q0FDcEUsQ0FBQyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFF1aWxsIGZyb20gJ3F1aWxsJztcblxuY29uc3QgcGFyY2htZW50ID0gUXVpbGwuaW1wb3J0KCdwYXJjaG1lbnQnKTtcbmNvbnN0IHBpeGVsTGV2ZWxzID0gWzEsIDIsIDMsIDQsIDUsIDYsIDcsIDhdO1xuY29uc3QgdGFiTXVsdGlwbGllciA9IDM7XG5cbmNsYXNzIEluZGVudEF0dHJpYnV0b3IgZXh0ZW5kcyAocGFyY2htZW50LkF0dHJpYnV0b3IuU3R5bGUgYXMge1xuICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgQHR5cGVzY3JpcHQtZXNsaW50L25vLWV4cGxpY2l0LWFueVxuICBuZXcgKGZvcm1hdE5hbWU6IHN0cmluZywgc3R5bGVQcm9wZXJ0eTogc3RyaW5nLCBhdHRyaWJ1dG9yT3B0aW9uczogdW5rbm93bik6IGFueTtcbn0pIHtcbiAgY29uc3RydWN0b3IoZm9ybWF0TmFtZTogc3RyaW5nLCBzdHlsZVByb3BlcnR5OiBzdHJpbmcsIGF0dHJpYnV0b3JPcHRpb25zOiB1bmtub3duKSB7XG4gICAgc3VwZXIoZm9ybWF0TmFtZSwgc3R5bGVQcm9wZXJ0eSwgYXR0cmlidXRvck9wdGlvbnMpO1xuICB9XG5cbiAgcHVibGljIGFkZChub2RlOiBIVE1MRWxlbWVudCwgdmFsdWU6IHN0cmluZyk6IGJvb2xlYW4ge1xuICAgIHJldHVybiBzdXBlci5hZGQobm9kZSwgYCR7K3ZhbHVlICogdGFiTXVsdGlwbGllcn1lbWApO1xuICB9XG5cbiAgcHVibGljIHZhbHVlKG5vZGU6IEhUTUxFbGVtZW50KTogbnVtYmVyIHwgdW5kZWZpbmVkIHtcbiAgICByZXR1cm4gcGFyc2VGbG9hdChzdXBlci52YWx1ZShub2RlKSkgLyB0YWJNdWx0aXBsaWVyIHx8IHVuZGVmaW5lZDsgLy8gRG9uJ3QgcmV0dXJuIE5hTlxuICB9XG59XG5cbmV4cG9ydCBjb25zdCBDdXN0b21JbmRlbnRTdHlsZUJsb3QgPSBuZXcgSW5kZW50QXR0cmlidXRvcignaW5kZW50JywgJ21hcmdpbi1sZWZ0Jywge1xuICBzY29wZTogcGFyY2htZW50LlNjb3BlLkJMT0NLLFxuICB3aGl0ZWxpc3Q6IHBpeGVsTGV2ZWxzLm1hcCgodmFsdWUpID0+IGAke3ZhbHVlICogdGFiTXVsdGlwbGllcn1lbWApLFxufSk7XG4iXX0=
20
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZW50LWF0dHJpYnV0b3IuY29tcG9uZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vLi4vLi4vbGlicy11aS9jb21wb25lbnRzL2lucHV0cy9xdWlsbC9zcmMvYmxvdHMtY3VzdG9tL2luZGVudC1hdHRyaWJ1dG9yLmNvbXBvbmVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssTUFBTSxPQUFPLENBQUM7QUFFMUIsTUFBTSxTQUFTLEdBQUcsS0FBSyxDQUFDLE1BQU0sQ0FBQyxXQUFXLENBQUMsQ0FBQztBQUM1QyxNQUFNLFdBQVcsR0FBRyxDQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQztBQUM3QyxNQUFNLGFBQWEsR0FBRyxDQUFDLENBQUM7QUFFeEIsTUFBTSxnQkFBaUIsU0FBUyxTQUFTLENBQUMsVUFBVSxDQUFDLEtBR25EO0lBQ0EsWUFBWSxVQUFrQixFQUFFLGFBQXFCLEVBQUUsaUJBQTBCO1FBQy9FLEtBQUssQ0FBQyxVQUFVLEVBQUUsYUFBYSxFQUFFLGlCQUFpQixDQUFDLENBQUMsQ0FBQyw4RkFBOEY7SUFDckosQ0FBQztJQUNNLEdBQUcsQ0FBQyxJQUFpQixFQUFFLEtBQWE7UUFDekMsT0FBTyxLQUFLLENBQUMsR0FBRyxDQUFDLElBQUksRUFBRSxHQUFHLENBQUMsS0FBSyxHQUFHLGFBQWEsSUFBSSxDQUFDLENBQUM7SUFDeEQsQ0FBQztJQUVNLEtBQUssQ0FBQyxJQUFpQjtRQUM1QixPQUFPLFVBQVUsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsYUFBYSxJQUFJLFNBQVMsQ0FBQyxDQUFDLG1CQUFtQjtJQUN4RixDQUFDO0NBQ0Y7QUFFRCxNQUFNLENBQUMsTUFBTSxxQkFBcUIsR0FBRyxJQUFJLGdCQUFnQixDQUFDLFFBQVEsRUFBRSxhQUFhLEVBQUU7SUFDakYsS0FBSyxFQUFFLFNBQVMsQ0FBQyxLQUFLLENBQUMsS0FBSztJQUM1QixTQUFTLEVBQUUsV0FBVyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsR0FBRyxLQUFLLEdBQUcsYUFBYSxJQUFJLENBQUM7Q0FDcEUsQ0FBQyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFF1aWxsIGZyb20gJ3F1aWxsJztcblxuY29uc3QgcGFyY2htZW50ID0gUXVpbGwuaW1wb3J0KCdwYXJjaG1lbnQnKTtcbmNvbnN0IHBpeGVsTGV2ZWxzID0gWzEsIDIsIDMsIDQsIDUsIDYsIDcsIDhdO1xuY29uc3QgdGFiTXVsdGlwbGllciA9IDM7XG5cbmNsYXNzIEluZGVudEF0dHJpYnV0b3IgZXh0ZW5kcyAocGFyY2htZW50LkF0dHJpYnV0b3IuU3R5bGUgYXMge1xuICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgQHR5cGVzY3JpcHQtZXNsaW50L25vLWV4cGxpY2l0LWFueVxuICBuZXcgKGZvcm1hdE5hbWU6IHN0cmluZywgc3R5bGVQcm9wZXJ0eTogc3RyaW5nLCBhdHRyaWJ1dG9yT3B0aW9uczogdW5rbm93bik6IGFueTtcbn0pIHtcbiAgY29uc3RydWN0b3IoZm9ybWF0TmFtZTogc3RyaW5nLCBzdHlsZVByb3BlcnR5OiBzdHJpbmcsIGF0dHJpYnV0b3JPcHRpb25zOiB1bmtub3duKSB7XG4gICAgc3VwZXIoZm9ybWF0TmFtZSwgc3R5bGVQcm9wZXJ0eSwgYXR0cmlidXRvck9wdGlvbnMpOyAvLyBOT1NPTkFSIOKAlCBkeW5hbWljIFF1aWxsIFBhcmNobWVudCBjbGFzcyBjYXN0IGFzIGNvbnN0cnVjdG9yLCBpbnRlbnRpb25hbCBUeXBlU2NyaXB0IHBhdHRlcm5cbiAgfVxuICBwdWJsaWMgYWRkKG5vZGU6IEhUTUxFbGVtZW50LCB2YWx1ZTogc3RyaW5nKTogYm9vbGVhbiB7XG4gICAgcmV0dXJuIHN1cGVyLmFkZChub2RlLCBgJHsrdmFsdWUgKiB0YWJNdWx0aXBsaWVyfWVtYCk7XG4gIH1cblxuICBwdWJsaWMgdmFsdWUobm9kZTogSFRNTEVsZW1lbnQpOiBudW1iZXIgfCB1bmRlZmluZWQge1xuICAgIHJldHVybiBwYXJzZUZsb2F0KHN1cGVyLnZhbHVlKG5vZGUpKSAvIHRhYk11bHRpcGxpZXIgfHwgdW5kZWZpbmVkOyAvLyBEb24ndCByZXR1cm4gTmFOXG4gIH1cbn1cblxuZXhwb3J0IGNvbnN0IEN1c3RvbUluZGVudFN0eWxlQmxvdCA9IG5ldyBJbmRlbnRBdHRyaWJ1dG9yKCdpbmRlbnQnLCAnbWFyZ2luLWxlZnQnLCB7XG4gIHNjb3BlOiBwYXJjaG1lbnQuU2NvcGUuQkxPQ0ssXG4gIHdoaXRlbGlzdDogcGl4ZWxMZXZlbHMubWFwKCh2YWx1ZSkgPT4gYCR7dmFsdWUgKiB0YWJNdWx0aXBsaWVyfWVtYCksXG59KTtcbiJdfQ==
@@ -31,4 +31,4 @@ export class QuillMentionBlot extends Embed {
31
31
  event.stopPropagation();
32
32
  }
33
33
  }
34
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWVudGlvbi5jb21wb25lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9saWJzLXVpL2NvbXBvbmVudHMvaW5wdXRzL3F1aWxsL3NyYy9ibG90cy1jdXN0b20vbWVudGlvbi5jb21wb25lbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE1BQU0sT0FBTyxDQUFDO0FBRTFCLE1BQU0sS0FBSyxHQUFHLEtBQUssQ0FBQyxNQUFNLENBQUMsYUFBYSxDQUFDLENBQUM7QUFFMUMsTUFBTSxPQUFPLGdCQUFpQixTQUFRLEtBQUs7SUFDekMsTUFBTSxDQUFDLFFBQVEsR0FBRyxTQUFTLENBQUM7SUFDNUIsTUFBTSxDQUFDLE9BQU8sR0FBRyxNQUFNLENBQUM7SUFDeEIsTUFBTSxDQUFDLFNBQVMsR0FBRyx1QkFBdUIsQ0FBQztJQUMzQyxNQUFNLENBQUMsTUFBTSxDQUFDLElBQWlEO1FBQzdELE1BQU0sSUFBSSxHQUFHLEtBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUM1QixJQUFJLENBQUMsU0FBUyxJQUFJLElBQUksQ0FBQyxLQUFLLENBQUM7UUFDN0IsSUFBSSxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDO1FBQ2pDLElBQUksQ0FBQyxZQUFZLENBQUMsTUFBTSxFQUFFLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUNyQyxJQUFJLENBQUMsWUFBWSxDQUFDLE9BQU8sRUFBRSxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDdkMsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0lBRUQsTUFBTSxDQUFDLEtBQUssQ0FBQyxPQUFvQjtRQUMvQixPQUFPO1lBQ0wsRUFBRSxFQUFFLE9BQU8sQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDO1lBQzlCLElBQUksRUFBRSxPQUFPLENBQUMsWUFBWSxDQUFDLE1BQU0sQ0FBQztZQUNsQyxLQUFLLEVBQUUsT0FBTyxDQUFDLFlBQVksQ0FBQyxPQUFPLENBQUM7U0FDckMsQ0FBQztJQUNKLENBQUM7SUFFRCxNQUFNO1FBQ0osS0FBSyxDQUFDLE1BQU0sRUFBRSxDQUFDO1FBQ2YsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztZQUNsQixJQUFJLENBQUMsT0FBTyxHQUFHLElBQUksQ0FBQztZQUNwQixJQUFJLENBQUMsWUFBWSxHQUFHLElBQUksQ0FBQyxlQUFlLENBQUM7WUFDekMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxnQkFBZ0IsQ0FBQyxPQUFPLEVBQUUsSUFBSSxDQUFDLFlBQVksRUFBRSxLQUFLLENBQUMsQ0FBQztRQUNuRSxDQUFDO0lBQ0gsQ0FBQztJQUVELGVBQWUsQ0FBQyxLQUFZO1FBQzFCLEtBQUssQ0FBQyxlQUFlLEVBQUUsQ0FBQztJQUMxQixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFF1aWxsIGZyb20gJ3F1aWxsJztcblxuY29uc3QgRW1iZWQgPSBRdWlsbC5pbXBvcnQoJ2Jsb3RzL2VtYmVkJyk7XG5cbmV4cG9ydCBjbGFzcyBRdWlsbE1lbnRpb25CbG90IGV4dGVuZHMgRW1iZWQge1xuICBzdGF0aWMgYmxvdE5hbWUgPSAnbWVudGlvbic7XG4gIHN0YXRpYyB0YWdOYW1lID0gJ3NwYW4nO1xuICBzdGF0aWMgY2xhc3NOYW1lID0gJ2xpYnMtdWktcXVpbGwtbWVudGlvbic7XG4gIHN0YXRpYyBjcmVhdGUoZGF0YTogeyBpZDogc3RyaW5nOyBmZUlkOiBzdHJpbmc7IHZhbHVlOiBzdHJpbmcgfSkge1xuICAgIGNvbnN0IG5vZGUgPSBzdXBlci5jcmVhdGUoKTtcbiAgICBub2RlLmlubmVyVGV4dCArPSBkYXRhLnZhbHVlO1xuICAgIG5vZGUuc2V0QXR0cmlidXRlKCdpZCcsIGRhdGEuaWQpO1xuICAgIG5vZGUuc2V0QXR0cmlidXRlKCdmZUlkJywgZGF0YS5mZUlkKTtcbiAgICBub2RlLnNldEF0dHJpYnV0ZSgndmFsdWUnLCBkYXRhLnZhbHVlKTtcbiAgICByZXR1cm4gbm9kZTtcbiAgfVxuXG4gIHN0YXRpYyB2YWx1ZShkb21Ob2RlOiBIVE1MRWxlbWVudCkge1xuICAgIHJldHVybiB7XG4gICAgICBpZDogZG9tTm9kZS5nZXRBdHRyaWJ1dGUoJ2lkJyksXG4gICAgICBmZUlkOiBkb21Ob2RlLmdldEF0dHJpYnV0ZSgnZmVJZCcpLFxuICAgICAgdmFsdWU6IGRvbU5vZGUuZ2V0QXR0cmlidXRlKCd2YWx1ZScpLFxuICAgIH07XG4gIH1cblxuICBhdHRhY2goKSB7XG4gICAgc3VwZXIuYXR0YWNoKCk7XG4gICAgaWYgKCF0aGlzLm1vdW50ZWQpIHtcbiAgICAgIHRoaXMubW91bnRlZCA9IHRydWU7XG4gICAgICB0aGlzLmNsaWNrSGFuZGxlciA9IHRoaXMuZ2V0Q2xpY2tIYW5kbGVyO1xuICAgICAgdGhpcy5kb21Ob2RlLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgdGhpcy5jbGlja0hhbmRsZXIsIGZhbHNlKTtcbiAgICB9XG4gIH1cblxuICBnZXRDbGlja0hhbmRsZXIoZXZlbnQ6IEV2ZW50KSB7XG4gICAgZXZlbnQuc3RvcFByb3BhZ2F0aW9uKCk7XG4gIH1cbn1cbiJdfQ==
34
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWVudGlvbi5jb21wb25lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9saWJzLXVpL2NvbXBvbmVudHMvaW5wdXRzL3F1aWxsL3NyYy9ibG90cy1jdXN0b20vbWVudGlvbi5jb21wb25lbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE1BQU0sT0FBTyxDQUFDO0FBRTFCLE1BQU0sS0FBSyxHQUFHLEtBQUssQ0FBQyxNQUFNLENBQUMsYUFBYSxDQUFDLENBQUM7QUFFMUMsTUFBTSxPQUFPLGdCQUFpQixTQUFRLEtBQUs7SUFDekMsTUFBTSxDQUFVLFFBQVEsR0FBRyxTQUFTLENBQUM7SUFDckMsTUFBTSxDQUFVLE9BQU8sR0FBRyxNQUFNLENBQUM7SUFDakMsTUFBTSxDQUFVLFNBQVMsR0FBRyx1QkFBdUIsQ0FBQztJQUNwRCxNQUFNLENBQUMsTUFBTSxDQUFDLElBQWlEO1FBQzdELE1BQU0sSUFBSSxHQUFHLEtBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUM1QixJQUFJLENBQUMsU0FBUyxJQUFJLElBQUksQ0FBQyxLQUFLLENBQUM7UUFDN0IsSUFBSSxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDO1FBQ2pDLElBQUksQ0FBQyxZQUFZLENBQUMsTUFBTSxFQUFFLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUNyQyxJQUFJLENBQUMsWUFBWSxDQUFDLE9BQU8sRUFBRSxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDdkMsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0lBRUQsTUFBTSxDQUFDLEtBQUssQ0FBQyxPQUFvQjtRQUMvQixPQUFPO1lBQ0wsRUFBRSxFQUFFLE9BQU8sQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDO1lBQzlCLElBQUksRUFBRSxPQUFPLENBQUMsWUFBWSxDQUFDLE1BQU0sQ0FBQztZQUNsQyxLQUFLLEVBQUUsT0FBTyxDQUFDLFlBQVksQ0FBQyxPQUFPLENBQUM7U0FDckMsQ0FBQztJQUNKLENBQUM7SUFFRCxNQUFNO1FBQ0osS0FBSyxDQUFDLE1BQU0sRUFBRSxDQUFDO1FBQ2YsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztZQUNsQixJQUFJLENBQUMsT0FBTyxHQUFHLElBQUksQ0FBQztZQUNwQixJQUFJLENBQUMsWUFBWSxHQUFHLElBQUksQ0FBQyxlQUFlLENBQUM7WUFDekMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxnQkFBZ0IsQ0FBQyxPQUFPLEVBQUUsSUFBSSxDQUFDLFlBQVksRUFBRSxLQUFLLENBQUMsQ0FBQztRQUNuRSxDQUFDO0lBQ0gsQ0FBQztJQUVELGVBQWUsQ0FBQyxLQUFZO1FBQzFCLEtBQUssQ0FBQyxlQUFlLEVBQUUsQ0FBQztJQUMxQixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFF1aWxsIGZyb20gJ3F1aWxsJztcblxuY29uc3QgRW1iZWQgPSBRdWlsbC5pbXBvcnQoJ2Jsb3RzL2VtYmVkJyk7XG5cbmV4cG9ydCBjbGFzcyBRdWlsbE1lbnRpb25CbG90IGV4dGVuZHMgRW1iZWQge1xuICBzdGF0aWMgcmVhZG9ubHkgYmxvdE5hbWUgPSAnbWVudGlvbic7XG4gIHN0YXRpYyByZWFkb25seSB0YWdOYW1lID0gJ3NwYW4nO1xuICBzdGF0aWMgcmVhZG9ubHkgY2xhc3NOYW1lID0gJ2xpYnMtdWktcXVpbGwtbWVudGlvbic7XG4gIHN0YXRpYyBjcmVhdGUoZGF0YTogeyBpZDogc3RyaW5nOyBmZUlkOiBzdHJpbmc7IHZhbHVlOiBzdHJpbmcgfSkge1xuICAgIGNvbnN0IG5vZGUgPSBzdXBlci5jcmVhdGUoKTtcbiAgICBub2RlLmlubmVyVGV4dCArPSBkYXRhLnZhbHVlO1xuICAgIG5vZGUuc2V0QXR0cmlidXRlKCdpZCcsIGRhdGEuaWQpO1xuICAgIG5vZGUuc2V0QXR0cmlidXRlKCdmZUlkJywgZGF0YS5mZUlkKTtcbiAgICBub2RlLnNldEF0dHJpYnV0ZSgndmFsdWUnLCBkYXRhLnZhbHVlKTtcbiAgICByZXR1cm4gbm9kZTtcbiAgfVxuXG4gIHN0YXRpYyB2YWx1ZShkb21Ob2RlOiBIVE1MRWxlbWVudCkge1xuICAgIHJldHVybiB7XG4gICAgICBpZDogZG9tTm9kZS5nZXRBdHRyaWJ1dGUoJ2lkJyksXG4gICAgICBmZUlkOiBkb21Ob2RlLmdldEF0dHJpYnV0ZSgnZmVJZCcpLFxuICAgICAgdmFsdWU6IGRvbU5vZGUuZ2V0QXR0cmlidXRlKCd2YWx1ZScpLFxuICAgIH07XG4gIH1cblxuICBhdHRhY2goKSB7XG4gICAgc3VwZXIuYXR0YWNoKCk7XG4gICAgaWYgKCF0aGlzLm1vdW50ZWQpIHtcbiAgICAgIHRoaXMubW91bnRlZCA9IHRydWU7XG4gICAgICB0aGlzLmNsaWNrSGFuZGxlciA9IHRoaXMuZ2V0Q2xpY2tIYW5kbGVyO1xuICAgICAgdGhpcy5kb21Ob2RlLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgdGhpcy5jbGlja0hhbmRsZXIsIGZhbHNlKTtcbiAgICB9XG4gIH1cblxuICBnZXRDbGlja0hhbmRsZXIoZXZlbnQ6IEV2ZW50KSB7XG4gICAgZXZlbnQuc3RvcFByb3BhZ2F0aW9uKCk7XG4gIH1cbn1cbiJdfQ==