@libs-ui/components-inputs-mention 0.2.356-9 → 0.2.357-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +357 -64
- package/esm2022/defines/utils.define.mjs +5 -6
- package/esm2022/list/list.component.mjs +2 -3
- package/esm2022/mention.directive.mjs +15 -16
- package/fesm2022/libs-ui-components-inputs-mention.mjs +21 -24
- package/fesm2022/libs-ui-components-inputs-mention.mjs.map +1 -1
- package/list/list.component.d.ts +4 -4
- package/mention.directive.d.ts +16 -16
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -1,147 +1,444 @@
|
|
|
1
1
|
# @libs-ui/components-inputs-mention
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Angular directive thêm tính năng mention (gợi ý khi gõ ký tự trigger) vào input, textarea hoặc contenteditable elements.
|
|
4
4
|
|
|
5
5
|
## Giới thiệu
|
|
6
6
|
|
|
7
|
-
`LibsUiComponentsInputsMentionDirective` là một standalone
|
|
7
|
+
`LibsUiComponentsInputsMentionDirective` là một Angular standalone directive cho phép người dùng tìm kiếm và chọn các mục từ danh sách khi gõ một ký tự kích hoạt (ví dụ: `@`, `#`). Directive hỗ trợ đầy đủ cả input/textarea thông thường lẫn contenteditable elements và các rich text editor như Quill. Danh sách gợi ý được render thông qua dynamic component, tự định vị theo vị trí con trỏ trong văn bản.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Tính năng
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
11
|
+
- Hỗ trợ nhiều ký tự trigger (`@`, `#`, ...) với cấu hình riêng biệt trong cùng một input
|
|
12
|
+
- Hoạt động trên `input`, `textarea` và các phần tử `contenteditable`
|
|
13
|
+
- Tích hợp sẵn với Quill editor (tự động phát hiện `.ql-editor`)
|
|
14
|
+
- Tùy chỉnh template hiển thị từng mục trong danh sách gợi ý qua `TemplateRef`
|
|
15
|
+
- Lọc danh sách theo từ khóa tìm kiếm (tích hợp sẵn hoặc custom filter)
|
|
16
|
+
- Điều hướng bàn phím: mũi tên lên/xuống, Enter để chọn, Escape để đóng
|
|
17
|
+
- Hỗ trợ iframe
|
|
18
|
+
- Expose `FunctionControl` để chèn mention thủ công từ bên ngoài
|
|
19
|
+
- Angular Signals + `ChangeDetectionStrategy.OnPush` cho hiệu năng tốt nhất
|
|
20
|
+
- Tự động dọn dẹp dynamic component khi directive bị destroy
|
|
18
21
|
|
|
19
22
|
## Khi nào sử dụng
|
|
20
23
|
|
|
21
|
-
- Khi
|
|
22
|
-
- Khi
|
|
23
|
-
- Khi
|
|
24
|
+
- Khi cần tính năng mention trong khung chat hoặc khu vực bình luận
|
|
25
|
+
- Khi muốn hỗ trợ phím tắt gắn thẻ người dùng nhanh trong các form nhập liệu
|
|
26
|
+
- Khi cần tích hợp mention vào các rich text editor (Quill, contenteditable)
|
|
27
|
+
- Khi muốn hỗ trợ nhiều loại trigger khác nhau trong cùng một vùng nhập (`@` cho người dùng, `#` cho tag)
|
|
28
|
+
- Khi cần chèn mention thủ công từ nút bên ngoài (qua `outFunctionControl`)
|
|
24
29
|
|
|
25
30
|
## Cài đặt
|
|
26
31
|
|
|
27
32
|
```bash
|
|
28
|
-
# npm
|
|
29
33
|
npm install @libs-ui/components-inputs-mention
|
|
30
|
-
|
|
31
|
-
# yarn
|
|
32
|
-
yarn add @libs-ui/components-inputs-mention
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
## Import
|
|
36
37
|
|
|
37
38
|
```typescript
|
|
38
39
|
import { LibsUiComponentsInputsMentionDirective } from '@libs-ui/components-inputs-mention';
|
|
40
|
+
import { IMentionConfig, IMentionInsert, IMentionFunctionControlEvent } from '@libs-ui/components-inputs-mention';
|
|
39
41
|
|
|
40
42
|
@Component({
|
|
41
43
|
standalone: true,
|
|
42
44
|
imports: [LibsUiComponentsInputsMentionDirective],
|
|
43
45
|
// ...
|
|
44
46
|
})
|
|
45
|
-
export class
|
|
47
|
+
export class MyComponent {}
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
## Ví dụ
|
|
50
|
+
## Ví dụ sử dụng
|
|
51
|
+
|
|
52
|
+
### 1. Cơ bản — input với trigger `@`
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
```typescript
|
|
55
|
+
import { Component } from '@angular/core';
|
|
56
|
+
import { LibsUiComponentsInputsMentionDirective, IMentionConfig } from '@libs-ui/components-inputs-mention';
|
|
57
|
+
|
|
58
|
+
@Component({
|
|
59
|
+
standalone: true,
|
|
60
|
+
imports: [LibsUiComponentsInputsMentionDirective],
|
|
61
|
+
template: `
|
|
62
|
+
<input
|
|
63
|
+
type="text"
|
|
64
|
+
class="w-full p-2 border rounded"
|
|
65
|
+
placeholder="Gõ @ để mention người dùng..."
|
|
66
|
+
LibsUiComponentsInputsMentionDirective
|
|
67
|
+
[mentionConfig]="basicConfig"
|
|
68
|
+
(outSearchTerm)="handlerSearchTerm($event)"
|
|
69
|
+
(outItemSelected)="handlerItemSelected($event)"
|
|
70
|
+
/>
|
|
71
|
+
`,
|
|
72
|
+
})
|
|
73
|
+
export class CommentInputComponent {
|
|
74
|
+
readonly basicConfig: IMentionConfig = {
|
|
75
|
+
items: [
|
|
76
|
+
{ id: '1', name: 'Anh Tuấn', username: 'tuan@example.com' },
|
|
77
|
+
{ id: '2', name: 'Bảo Ngọc', username: 'ngoc@example.com' },
|
|
78
|
+
{ id: '3', name: 'Công Thành', username: 'thanh@example.com' },
|
|
79
|
+
],
|
|
80
|
+
triggerChar: '@',
|
|
81
|
+
labelKey: 'name',
|
|
82
|
+
mentionFilter: (search, items) => {
|
|
83
|
+
const s = search.toLowerCase();
|
|
84
|
+
return items?.filter((item: any) => item.name.toLowerCase().includes(s));
|
|
85
|
+
},
|
|
86
|
+
mentionSelect: (item: any, triggerChar) => `${triggerChar}${item.name}`,
|
|
87
|
+
};
|
|
51
88
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
89
|
+
handlerSearchTerm(term: string | undefined): void {
|
|
90
|
+
// term là chuỗi người dùng đang gõ sau @
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
handlerItemSelected(item: unknown): void {
|
|
94
|
+
// item là object được chọn từ danh sách
|
|
95
|
+
}
|
|
96
|
+
}
|
|
57
97
|
```
|
|
58
98
|
|
|
59
|
-
###
|
|
99
|
+
### 2. Nhiều trigger — `@` cho người dùng và `#` cho tag
|
|
60
100
|
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
```typescript
|
|
102
|
+
import { Component } from '@angular/core';
|
|
103
|
+
import { LibsUiComponentsInputsMentionDirective, IMentionConfig } from '@libs-ui/components-inputs-mention';
|
|
104
|
+
|
|
105
|
+
@Component({
|
|
106
|
+
standalone: true,
|
|
107
|
+
imports: [LibsUiComponentsInputsMentionDirective],
|
|
108
|
+
template: `
|
|
109
|
+
<textarea
|
|
110
|
+
class="w-full p-2 border rounded min-h-[100px]"
|
|
111
|
+
placeholder="Gõ @ cho người dùng, # cho tags..."
|
|
112
|
+
LibsUiComponentsInputsMentionDirective
|
|
113
|
+
[mentionConfig]="multiConfig"
|
|
114
|
+
></textarea>
|
|
115
|
+
`,
|
|
116
|
+
})
|
|
117
|
+
export class MultiTriggerComponent {
|
|
118
|
+
readonly users = [
|
|
119
|
+
{ id: '1', name: 'Anh Tuấn', username: 'tuan' },
|
|
120
|
+
{ id: '2', name: 'Bảo Ngọc', username: 'ngoc' },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
readonly tags = [
|
|
124
|
+
{ id: 't1', name: 'angular', username: 'AG' },
|
|
125
|
+
{ id: 't2', name: 'typescript', username: 'TS' },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
readonly filterByName = (search: string, items?: Array<any>) =>
|
|
129
|
+
items?.filter((item) => item.name.toLowerCase().includes(search.toLowerCase()));
|
|
130
|
+
|
|
131
|
+
readonly multiConfig: IMentionConfig = {
|
|
132
|
+
items: this.users,
|
|
67
133
|
triggerChar: '@',
|
|
134
|
+
labelKey: 'name',
|
|
135
|
+
mentionFilter: this.filterByName,
|
|
136
|
+
mentionSelect: (item: any, triggerChar) => `${triggerChar}${item.name}`,
|
|
68
137
|
mention: [
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
138
|
+
{
|
|
139
|
+
items: this.tags,
|
|
140
|
+
triggerChar: '#',
|
|
141
|
+
labelKey: 'name',
|
|
142
|
+
mentionFilter: this.filterByName,
|
|
143
|
+
mentionSelect: (item: any, triggerChar) => `${triggerChar}${item.name}`,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
72
148
|
```
|
|
73
149
|
|
|
74
|
-
|
|
150
|
+
### 3. ContentEditable với custom template danh sách
|
|
75
151
|
|
|
76
|
-
|
|
152
|
+
```typescript
|
|
153
|
+
import { Component } from '@angular/core';
|
|
154
|
+
import { LibsUiComponentsInputsMentionDirective, IMentionConfig } from '@libs-ui/components-inputs-mention';
|
|
77
155
|
|
|
78
|
-
|
|
156
|
+
@Component({
|
|
157
|
+
standalone: true,
|
|
158
|
+
imports: [LibsUiComponentsInputsMentionDirective],
|
|
159
|
+
template: `
|
|
160
|
+
<div
|
|
161
|
+
contenteditable="true"
|
|
162
|
+
class="w-full p-4 border rounded bg-white min-h-[100px]"
|
|
163
|
+
LibsUiComponentsInputsMentionDirective
|
|
164
|
+
[mentionConfig]="basicConfig"
|
|
165
|
+
[mentionListTemplate]="customTpl"
|
|
166
|
+
>
|
|
167
|
+
Thử gõ @ ở đây...
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<ng-template #customTpl let-item="item">
|
|
171
|
+
<div class="flex items-center gap-3 p-3 hover:bg-blue-50 cursor-pointer">
|
|
172
|
+
<div class="w-8 h-8 rounded-full bg-blue-500 text-white flex items-center justify-center font-bold">
|
|
173
|
+
{{ item.name[0] }}
|
|
174
|
+
</div>
|
|
175
|
+
<div>
|
|
176
|
+
<div class="font-medium text-gray-800 text-sm">{{ item.name }}</div>
|
|
177
|
+
<div class="text-xs text-gray-500">{{ item.username }}</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</ng-template>
|
|
181
|
+
`,
|
|
182
|
+
})
|
|
183
|
+
export class ContentEditableMentionComponent {
|
|
184
|
+
readonly basicConfig: IMentionConfig = {
|
|
185
|
+
items: [
|
|
186
|
+
{ id: '1', name: 'Anh Tuấn', username: 'tuan@example.com' },
|
|
187
|
+
{ id: '2', name: 'Bảo Ngọc', username: 'ngoc@example.com' },
|
|
188
|
+
],
|
|
189
|
+
triggerChar: '@',
|
|
190
|
+
labelKey: 'name',
|
|
191
|
+
mentionFilter: (search, items) =>
|
|
192
|
+
items?.filter((item: any) => item.name.toLowerCase().includes(search.toLowerCase())),
|
|
193
|
+
mentionSelect: (item: any) => `@${item.name}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
```
|
|
79
197
|
|
|
80
|
-
|
|
81
|
-
| ----------------------- | -------------------------------- | ----------- | ------------------------------------------------- |
|
|
82
|
-
| `[timeDelayInit]` | `number` | `0` | Thời gian trễ trước khi khởi tạo directive logic. |
|
|
83
|
-
| `[mentionConfig]` | `IMentionConfig` | `undefined` | Cấu hình chính cho mention. |
|
|
84
|
-
| `[mentionListTemplate]` | `TemplateRef<TYPE_TEMPLATE_REF>` | `undefined` | Template tùy chỉnh cho danh sách gợi ý. |
|
|
198
|
+
### 4. Chèn mention thủ công từ nút bên ngoài
|
|
85
199
|
|
|
86
|
-
|
|
200
|
+
```typescript
|
|
201
|
+
import { Component } from '@angular/core';
|
|
202
|
+
import { LibsUiComponentsInputsMentionDirective, IMentionConfig, IMentionFunctionControlEvent } from '@libs-ui/components-inputs-mention';
|
|
87
203
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
204
|
+
@Component({
|
|
205
|
+
standalone: true,
|
|
206
|
+
imports: [LibsUiComponentsInputsMentionDirective],
|
|
207
|
+
template: `
|
|
208
|
+
<div
|
|
209
|
+
contenteditable="true"
|
|
210
|
+
class="w-full p-3 border rounded min-h-[80px]"
|
|
211
|
+
LibsUiComponentsInputsMentionDirective
|
|
212
|
+
[mentionConfig]="config"
|
|
213
|
+
(outFunctionControl)="handlerFunctionControl($event)"
|
|
214
|
+
></div>
|
|
215
|
+
|
|
216
|
+
<button
|
|
217
|
+
class="mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
|
218
|
+
(click)="handlerAddMention($event)"
|
|
219
|
+
>
|
|
220
|
+
Thêm @mention
|
|
221
|
+
</button>
|
|
222
|
+
`,
|
|
223
|
+
})
|
|
224
|
+
export class ManualMentionComponent {
|
|
225
|
+
private mentionControl: IMentionFunctionControlEvent | undefined;
|
|
226
|
+
|
|
227
|
+
readonly config: IMentionConfig = {
|
|
228
|
+
items: [
|
|
229
|
+
{ id: '1', name: 'Anh Tuấn', username: 'tuan' },
|
|
230
|
+
{ id: '2', name: 'Bảo Ngọc', username: 'ngoc' },
|
|
231
|
+
],
|
|
232
|
+
triggerChar: '@',
|
|
233
|
+
labelKey: 'name',
|
|
234
|
+
mentionFilter: (search, items) =>
|
|
235
|
+
items?.filter((item: any) => item.name.toLowerCase().includes(search.toLowerCase())),
|
|
236
|
+
mentionSelect: (item: any) => `@${item.name}`,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
handlerFunctionControl(event: IMentionFunctionControlEvent): void {
|
|
240
|
+
this.mentionControl = event;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
handlerAddMention(event: Event): void {
|
|
244
|
+
event.stopPropagation();
|
|
245
|
+
this.mentionControl?.addMention(false);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 5. Lắng nghe sự kiện chèn mention (Rich Text Editor)
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { Component } from '@angular/core';
|
|
254
|
+
import { LibsUiComponentsInputsMentionDirective, IMentionConfig, IMentionInsert } from '@libs-ui/components-inputs-mention';
|
|
255
|
+
|
|
256
|
+
@Component({
|
|
257
|
+
standalone: true,
|
|
258
|
+
imports: [LibsUiComponentsInputsMentionDirective],
|
|
259
|
+
template: `
|
|
260
|
+
<div
|
|
261
|
+
contenteditable="true"
|
|
262
|
+
class="w-full p-3 border rounded min-h-[80px]"
|
|
263
|
+
LibsUiComponentsInputsMentionDirective
|
|
264
|
+
[mentionConfig]="config"
|
|
265
|
+
(outInsertMention)="handlerInsertMention($event)"
|
|
266
|
+
(outToggle)="handlerToggle($event)"
|
|
267
|
+
></div>
|
|
268
|
+
`,
|
|
269
|
+
})
|
|
270
|
+
export class RteIntegrationComponent {
|
|
271
|
+
readonly config: IMentionConfig = {
|
|
272
|
+
items: [{ id: '1', name: 'Anh Tuấn', username: 'tuan@example.com' }],
|
|
273
|
+
triggerChar: '@',
|
|
274
|
+
labelKey: 'name',
|
|
275
|
+
mentionFilter: (search, items) =>
|
|
276
|
+
items?.filter((item: any) => item.name.toLowerCase().includes(search.toLowerCase())),
|
|
277
|
+
mentionSelect: (item: any) => `@${item.name}`,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
handlerInsertMention(event: IMentionInsert): void {
|
|
281
|
+
event; // { data: { id, feId, value }, lengthKey }
|
|
282
|
+
// Dùng trong Quill: editor.deleteText(pos - event.lengthKey, event.lengthKey)
|
|
283
|
+
// rồi editor.insertEmbed(pos, 'mention', event.data)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
handlerToggle(isOpen: boolean): void {
|
|
287
|
+
// isOpen = true khi danh sách gợi ý hiện, false khi đóng
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## @Input()
|
|
293
|
+
|
|
294
|
+
| Input | Type | Default | Mô tả | Ví dụ |
|
|
295
|
+
|---|---|---|---|---|
|
|
296
|
+
| `[timeDelayInit]` | `number` | `0` | Thời gian delay (ms) trước khi directive gắn event listeners. Hữu ích khi host element cần render xong trước. | `[timeDelayInit]="500"` |
|
|
297
|
+
| `[mentionConfig]` | `IMentionConfig` | `undefined` | Cấu hình chính cho mention: danh sách items, ký tự trigger, các tùy chọn filter/select. Bắt buộc phải truyền để directive hoạt động. | `[mentionConfig]="myConfig"` |
|
|
298
|
+
| `[mentionListTemplate]` | `TemplateRef<any>` | `undefined` | Template tùy chỉnh cho từng mục trong danh sách gợi ý. Context: `{ item, index }`. Nếu không truyền, dùng template mặc định (avatar + tên + username). | `[mentionListTemplate]="myTpl"` |
|
|
299
|
+
|
|
300
|
+
## @Output()
|
|
301
|
+
|
|
302
|
+
| Output | Type | Mô tả | Handler TS | Binding HTML |
|
|
303
|
+
|---|---|---|---|---|
|
|
304
|
+
| `(outSearchTerm)` | `string \| undefined` | Emits từ khóa người dùng đang gõ sau ký tự trigger. Dùng để load data bất đồng bộ từ API. | `handlerSearchTerm(term: string \| undefined): void { term; }` | `(outSearchTerm)="handlerSearchTerm($event)"` |
|
|
305
|
+
| `(outItemSelected)` | `unknown` | Emits object mục được chọn từ danh sách (trước khi chèn vào input). | `handlerItemSelected(item: unknown): void { item; }` | `(outItemSelected)="handlerItemSelected($event)"` |
|
|
306
|
+
| `(outToggle)` | `boolean` | Emits `true` khi danh sách gợi ý mở ra, `false` khi đóng lại. | `handlerToggle(isOpen: boolean): void { isOpen; }` | `(outToggle)="handlerToggle($event)"` |
|
|
307
|
+
| `(outInsertMention)` | `IMentionInsert` | Emits khi một mention được chèn thành công. Dùng cho rich text editor cần xử lý chèn node thủ công. | `handlerInsertMention(e: IMentionInsert): void { e.data; e.lengthKey; }` | `(outInsertMention)="handlerInsertMention($event)"` |
|
|
308
|
+
| `(outFunctionControl)` | `IMentionFunctionControlEvent` | Emits object chứa phương thức điều khiển directive từ bên ngoài (ví dụ: chèn mention thủ công). Emits một lần sau `ngAfterViewInit`. | `handlerFunctionControl(ctrl: IMentionFunctionControlEvent): void { this.ctrl = ctrl; }` | `(outFunctionControl)="handlerFunctionControl($event)"` |
|
|
95
309
|
|
|
96
310
|
## Types & Interfaces
|
|
97
311
|
|
|
98
312
|
```typescript
|
|
313
|
+
import {
|
|
314
|
+
IMentionConfig,
|
|
315
|
+
IMentionItem,
|
|
316
|
+
IMentionInsert,
|
|
317
|
+
IMentionFunctionControlEvent,
|
|
318
|
+
INodeInsert,
|
|
319
|
+
} from '@libs-ui/components-inputs-mention';
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
/**
|
|
324
|
+
* Cấu hình chính truyền vào [mentionConfig].
|
|
325
|
+
* Kế thừa IMentionItem và bổ sung các tùy chọn giao diện + đa trigger.
|
|
326
|
+
*/
|
|
99
327
|
export interface IMentionConfig extends IMentionItem {
|
|
328
|
+
/** Tắt style mặc định của danh sách gợi ý */
|
|
100
329
|
disableStyle?: boolean;
|
|
330
|
+
/** ID của iframe chứa host element (khi dùng trong iframe) */
|
|
101
331
|
iframe?: string;
|
|
332
|
+
/** Cấu hình cho các trigger bổ sung (ngoài trigger chính) */
|
|
102
333
|
mention?: Array<IMentionItem>;
|
|
103
334
|
}
|
|
104
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Cấu hình cho từng trigger riêng lẻ.
|
|
338
|
+
*/
|
|
105
339
|
export interface IMentionItem {
|
|
340
|
+
/** Ký tự kích hoạt danh sách gợi ý. Mặc định: '@' */
|
|
106
341
|
triggerChar?: string;
|
|
342
|
+
/** Tên field trong object dùng làm nhãn hiển thị. Mặc định: 'email' */
|
|
107
343
|
labelKey?: string;
|
|
344
|
+
/** Tắt tự động sắp xếp danh sách items theo labelKey */
|
|
108
345
|
disableSort?: boolean;
|
|
346
|
+
/** Tắt bộ lọc nội bộ (dùng khi tự xử lý filter bằng outSearchTerm + API) */
|
|
109
347
|
disableSearch?: boolean;
|
|
348
|
+
/** Hiển thị danh sách phía trên con trỏ thay vì bên dưới */
|
|
110
349
|
dropUp?: boolean;
|
|
350
|
+
/** Cho phép nhập khoảng trắng khi đang search mention */
|
|
111
351
|
allowSpace?: boolean;
|
|
352
|
+
/** Giới hạn số khoảng trắng tối đa trong search query */
|
|
112
353
|
limitSpaceSearchQuery?: number;
|
|
354
|
+
/** Bao gồm ký tự trigger trong giá trị emit ra outSearchTerm */
|
|
113
355
|
returnTrigger?: boolean;
|
|
356
|
+
/** Hàm format text được chèn vào input sau khi chọn item */
|
|
114
357
|
mentionSelect?: (item: unknown, triggerChar?: string) => string;
|
|
358
|
+
/** Hàm lọc danh sách items theo search string */
|
|
115
359
|
mentionFilter?: (searchString: string, items?: Array<unknown>) => Array<unknown> | undefined;
|
|
360
|
+
/** Sự kiện DOM trên mention span đã chèn kích hoạt mentionActionByEvent */
|
|
116
361
|
mentionEventName?: 'click' | 'mouseenter';
|
|
362
|
+
/** Callback được gọi khi sự kiện mentionEventName xảy ra trên span mention */
|
|
117
363
|
mentionActionByEvent?: (item: unknown, triggerChars?: string) => void;
|
|
364
|
+
/** Danh sách items hiển thị trong danh sách gợi ý. Bắt buộc. */
|
|
118
365
|
items: Array<Record<string, string | undefined>>;
|
|
119
|
-
|
|
366
|
+
/** z-index của dropdown danh sách gợi ý. Mặc định: 2500 */
|
|
120
367
|
zIndex?: number;
|
|
121
368
|
}
|
|
122
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Dữ liệu emit qua outInsertMention khi một mention được chèn thành công.
|
|
372
|
+
* Dùng cho rich text editor tích hợp sâu (Quill, ProseMirror).
|
|
373
|
+
*/
|
|
123
374
|
export interface IMentionInsert {
|
|
124
375
|
data: {
|
|
376
|
+
/** ID định danh duy nhất (từ item.id hoặc uuid tự sinh) */
|
|
125
377
|
id: string;
|
|
378
|
+
/** Frontend ID dùng để track node trong DOM */
|
|
126
379
|
feId: string;
|
|
380
|
+
/** Giá trị text được chèn (kết quả của mentionSelect) */
|
|
127
381
|
value: string;
|
|
128
382
|
};
|
|
383
|
+
/** Số ký tự cần xóa tính từ vị trí con trỏ để thay bằng mention node */
|
|
129
384
|
lengthKey: number;
|
|
130
385
|
}
|
|
131
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Object emit qua outFunctionControl để điều khiển directive từ bên ngoài.
|
|
389
|
+
*/
|
|
132
390
|
export interface IMentionFunctionControlEvent {
|
|
391
|
+
/**
|
|
392
|
+
* Chèn ký tự trigger vào vị trí con trỏ hiện tại và mở danh sách gợi ý.
|
|
393
|
+
* @param inputEvent - true nếu gọi từ sự kiện input
|
|
394
|
+
*/
|
|
133
395
|
addMention: (inputEvent: boolean) => void;
|
|
134
396
|
}
|
|
135
397
|
```
|
|
136
398
|
|
|
137
|
-
##
|
|
399
|
+
## Cấu hình mặc định (DEFAULT_CONFIG)
|
|
400
|
+
|
|
401
|
+
Khi không truyền đủ các option, directive áp dụng các giá trị mặc định sau:
|
|
402
|
+
|
|
403
|
+
| Thuộc tính | Giá trị mặc định |
|
|
404
|
+
|---|---|
|
|
405
|
+
| `triggerChar` | `'@'` |
|
|
406
|
+
| `labelKey` | `'email'` |
|
|
407
|
+
| `allowSpace` | `true` |
|
|
408
|
+
| `returnTrigger` | `false` |
|
|
409
|
+
| `limitSpaceSearchQuery` | `3` |
|
|
410
|
+
| `mentionEventName` | `'click'` |
|
|
411
|
+
| `mentionSelect` | `(item) => '@' + item.email` |
|
|
412
|
+
| `mentionFilter` | Lọc theo field `name` và `username` (hỗ trợ tiếng Việt bỏ dấu) |
|
|
413
|
+
| `zIndex` | `2500` |
|
|
414
|
+
|
|
415
|
+
## Điều hướng bàn phím
|
|
416
|
+
|
|
417
|
+
| Phím | Hành động |
|
|
418
|
+
|---|---|
|
|
419
|
+
| `@`, `#`, ... (trigger char) | Mở danh sách gợi ý |
|
|
420
|
+
| Mũi tên xuống | Chọn item tiếp theo |
|
|
421
|
+
| Mũi tên lên | Chọn item trước đó |
|
|
422
|
+
| `Enter` hoặc `Tab` | Chèn item đang active vào input |
|
|
423
|
+
| `Escape` | Đóng danh sách gợi ý |
|
|
424
|
+
| `Backspace` về trước ký tự trigger | Đóng danh sách gợi ý |
|
|
425
|
+
| `Space` (khi `allowSpace: false`) | Đóng danh sách gợi ý |
|
|
138
426
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
427
|
+
## Lưu ý quan trọng
|
|
428
|
+
|
|
429
|
+
⚠️ **mentionConfig là bắt buộc**: Nếu không truyền `[mentionConfig]`, directive sẽ không gắn event listeners và không hoạt động. Kiểm tra `ngAfterViewInit` trong source.
|
|
430
|
+
|
|
431
|
+
⚠️ **ContentEditable vs Input**: Logic xử lý caret và chèn mention khác nhau giữa `input`/`textarea` và `contenteditable`. Với `input`/`textarea`, directive dùng `selectionStart`. Với `contenteditable`, directive dùng `window.getSelection()` để tính vị trí và chèn HTML node.
|
|
432
|
+
|
|
433
|
+
⚠️ **outInsertMention chỉ emit với contenteditable**: Output `(outInsertMention)` chỉ được emit khi host element là `contenteditable` (mode editor). Với `input`/`textarea`, mention được chèn trực tiếp vào `value`.
|
|
434
|
+
|
|
435
|
+
⚠️ **Dependency bắt buộc**: Directive phụ thuộc vào `LibsUiDynamicComponentService` để render danh sách gợi ý. Đảm bảo service này được cung cấp trong ứng dụng.
|
|
436
|
+
|
|
437
|
+
⚠️ **mentionFilter phải được truyền để lọc hoạt động**: Nếu không truyền `mentionFilter`, directive dùng hàm mặc định lọc theo field `name` và `username`. Nếu data của bạn dùng field khác, hãy truyền hàm `mentionFilter` riêng.
|
|
438
|
+
|
|
439
|
+
⚠️ **timeDelayInit với lazy-loaded editor**: Khi dùng cùng Quill hoặc editor khởi tạo bất đồng bộ, truyền `[timeDelayInit]="500"` hoặc giá trị phù hợp để đảm bảo editor đã render xong trước khi directive gắn event.
|
|
440
|
+
|
|
441
|
+
⚠️ **disableSearch để load data từ API**: Khi muốn tự load danh sách từ API dựa trên từ khóa, set `disableSearch: true` trong config và lắng nghe `(outSearchTerm)` để gọi API. Sau khi nhận kết quả, cập nhật `mentionConfig.items` để danh sách hiển thị mới.
|
|
145
442
|
|
|
146
443
|
## Demo
|
|
147
444
|
|
|
@@ -149,8 +446,4 @@ export interface IMentionFunctionControlEvent {
|
|
|
149
446
|
npx nx serve core-ui
|
|
150
447
|
```
|
|
151
448
|
|
|
152
|
-
Truy cập: `http://localhost:
|
|
153
|
-
|
|
154
|
-
## License
|
|
155
|
-
|
|
156
|
-
MIT
|
|
449
|
+
Truy cập: `http://localhost:4500/inputs/mention`
|