@acorex/components 22.4.15 → 22.4.17

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/comment/README.md CHANGED
@@ -1,3 +1,468 @@
1
- # @acorex/components/comment
1
+ # Comment (`@acorex/components/comment`)
2
2
 
3
- Secondary entry point of `@acorex/components`. It can be used by importing from `@acorex/components/comment`.
3
+ کامپوننت ترد کامنت برای Angular. داده را به‌صورت درخت (`AXCommentItem`) می‌گیرد، لایک/ریپلای/اکشن را مدیریت می‌کند، و ادیتور را در فوتر نگه می‌دارد.
4
+
5
+ ## نصب و import
6
+
7
+ ```ts
8
+ import {
9
+ AXCommentModule,
10
+ AXCommentViewComponent,
11
+ AXCommentReplyComponent,
12
+ AXCommentItemTemplateDirective,
13
+ type AXCommentItem,
14
+ type AXCommentCallback,
15
+ type AXCommentActionEvent,
16
+ type AXCommentReplyEvent,
17
+ } from '@acorex/components/comment';
18
+ import { AXTextEditorModule } from '@acorex/components/text-editor';
19
+ import { AXDialogService } from '@acorex/components/dialog';
20
+ ```
21
+
22
+ Standalone:
23
+
24
+ ```ts
25
+ imports: [AXCommentModule, AXTextEditorModule, FormsModule]
26
+ ```
27
+
28
+ ---
29
+
30
+ ## معماری
31
+
32
+ | قطعه | سلکتور | نقش |
33
+ | --- | --- | --- |
34
+ | `AXCommentContainerComponent` | `ax-comment-container` | شِل با اسلات `ax-header` / `ax-footer` |
35
+ | `AXCommentViewComponent` | `ax-comment-view` | درخت داده، lazy load، متدهای mutate |
36
+ | `AXCommentItemComponent` | `ax-comment-item` | یک کامنت (لی‌آوت پیش‌فرض یا تمپلیت) |
37
+ | `AXCommentReplyComponent` | `ax-comment-reply` | نوار «در حال پاسخ به» بالای ادیتور |
38
+ | `AXCommentItemTemplateDirective` | `ng-template[axCommentItem]` | سفارشی‌سازی بدنه آیتم |
39
+
40
+ بدون `datasource` می‌توان آیتم‌ها را به‌صورت declarative داخل `ax-comment-view` پروجکت کرد.
41
+
42
+ ---
43
+
44
+ ## مدل داده `AXCommentItem`
45
+
46
+ ```ts
47
+ interface AXCommentItem {
48
+ id: string | number;
49
+ author: string;
50
+ avatar?: string;
51
+ initials?: string;
52
+ color?: AXStyleColorType;
53
+ date?: string;
54
+ edited?: boolean; // برچسب «ویرایش‌شده» قبل از زمان
55
+ editedAt?: string; // tooltip روی برچسب (اختیاری)
56
+ content?: string; // HTML مجاز است؛ در UI پیش‌فرض با innerHTML رندر می‌شود
57
+ likes?: number;
58
+ liked?: boolean;
59
+ isYou?: boolean; // کنار نام، badge «You»
60
+ repliesExpanded?: boolean;
61
+ replies?: AXCommentItem[]; // eager
62
+ replyCount?: number; // برای lazy؛ اگر replies خالی باشد از این استفاده می‌شود
63
+ actions?: AXMenuItem[]; // منوی ... فقط در UI پیش‌فرض
64
+ data?: unknown;
65
+ }
66
+ ```
67
+
68
+ `actions` همان `AXMenuItem` است:
69
+
70
+ ```ts
71
+ const actions: AXMenuItem[] = [
72
+ { name: 'edit', text: 'Edit', icon: 'ax-icon ax-icon-pen', break: true },
73
+ { name: 'delete', text: 'Delete', icon: 'ax-icon ax-icon-trash', color: 'danger' },
74
+ ];
75
+ ```
76
+
77
+ ---
78
+
79
+ ## ۱) استفاده پیش‌فرض (بدون تمپلیت)
80
+
81
+ Like و Reply داخل خود آیتم کار می‌کنند. رویدادها اختیاری‌اند.
82
+
83
+ ```html
84
+ <ax-comment-container>
85
+ <ax-comment-view
86
+ #commentsView
87
+ [datasource]="comments"
88
+ (onReply)="onReply($event)"
89
+ (onLikeChange)="onLikeChange($event)"
90
+ (onRepliesToggle)="onRepliesToggle($event)"
91
+ (onAction)="onAction($event)"
92
+ />
93
+
94
+ <ax-footer>
95
+ <ax-comment-reply [item]="replyingTo()" (onClose)="cancelReply()">
96
+ <ax-text-editor
97
+ #commentEditor
98
+ look="solid"
99
+ outputFormat="html"
100
+ [initialLine]="3"
101
+ [ngModel]="draft()"
102
+ (ngModelChange)="draft.set($event)"
103
+ [placeHolder]="replyingTo() ? 'Write your content...' : 'Write a comment...'"
104
+ >
105
+ <ax-footer>
106
+ <ax-button class="ax-sm" look="solid" color="primary" text="Send" (onClick)="send()" />
107
+ </ax-footer>
108
+ <ax-text-editor-toolbar>
109
+ <ax-text-editor-history />
110
+ <ax-text-editor-font-style />
111
+ <ax-text-editor-colors />
112
+ <ax-text-editor-list />
113
+ <ax-text-editor-alignment />
114
+ <ax-text-editor-media />
115
+ </ax-text-editor-toolbar>
116
+ </ax-text-editor>
117
+ </ax-comment-reply>
118
+ </ax-footer>
119
+ </ax-comment-container>
120
+ ```
121
+
122
+ ```ts
123
+ comments = signal<AXCommentItem[]>([
124
+ {
125
+ id: 1,
126
+ author: 'Heidi Thonig',
127
+ initials: 'H',
128
+ color: 'success',
129
+ date: '18 hours ago',
130
+ content: 'The new layout looks great!',
131
+ likes: 1,
132
+ liked: true,
133
+ repliesExpanded: true,
134
+ actions,
135
+ replies: [
136
+ {
137
+ id: 2,
138
+ author: 'You',
139
+ initials: 'Y',
140
+ isYou: true,
141
+ date: 'Just now',
142
+ content: 'Agreed.',
143
+ likes: 0,
144
+ actions,
145
+ },
146
+ ],
147
+ },
148
+ ]);
149
+ ```
150
+
151
+ سه نقطه (`...`) فقط در لی‌آوت پیش‌فرض است، روی هاور همان کامنت ظاهر می‌شود، و از `item.actions` می‌آید.
152
+
153
+ ---
154
+
155
+ ## ۲) Lazy loading
156
+
157
+ `datasource` می‌تواند تابع باشد:
158
+
159
+ - بدون `id` → ریشه‌ها
160
+ - با `id` → پاسخ‌های همان کامنت
161
+
162
+ برای ریشه، `replies` را نفرستید و `replyCount` بگذارید. وقتی کاربر ترد را باز کند، view دوباره callback را با id والد صدا می‌زند.
163
+
164
+ ```ts
165
+ readonly loadComments: AXCommentCallback = (id) => {
166
+ if (!id) {
167
+ return this.api.listRoots().then((rows) =>
168
+ rows.map((row) => ({
169
+ ...row,
170
+ replyCount: row.replyCount ?? 0,
171
+ // replies را حذف کنید تا lazy بماند
172
+ })),
173
+ );
174
+ }
175
+ return this.api.listReplies(id);
176
+ };
177
+ ```
178
+
179
+ ```html
180
+ <ax-comment-view [datasource]="loadComments" (onReply)="onReply($event)" />
181
+ ```
182
+
183
+ اسکلتون تا لود ریشه نشان داده می‌شود. باز/بسته کردن پاسخ‌ها `(onRepliesToggle)` می‌دهد.
184
+
185
+ برنامه می‌تواند از بیرون هم باز کند:
186
+
187
+ ```ts
188
+ this.commentsView()?.setExpanded('1', true);
189
+ ```
190
+
191
+ ---
192
+
193
+ ## ۳) تمپلیت سفارشی
194
+
195
+ بدنه آیتم عوض می‌شود؛ ردیف Like / Reply / تعداد پاسخ‌ها همان chrome پیش‌فرض می‌ماند. منوی `...` در تمپلیت سفارشی نشان داده نمی‌شود.
196
+
197
+ ```html
198
+ <ax-comment-view [datasource]="comments()" (onReply)="onReply($event)" (onLikeChange)="onLike($event)">
199
+ <ng-template axCommentItem let-item let-level="level" let-reply="reply">
200
+ <div class="my-card" [class.is-reply]="level > 0">
201
+ <ax-avatar [color]="item.color" [size]="48" look="solid" shape="soft">
202
+ <ax-text>{{ item.initials }}</ax-text>
203
+ </ax-avatar>
204
+ <ax-title>{{ item.author }}</ax-title>
205
+ <ax-content>
206
+ <div [innerHTML]="item.content"></div>
207
+ </ax-content>
208
+ </div>
209
+ </ng-template>
210
+ </ax-comment-view>
211
+ ```
212
+
213
+ کانتکست تمپلیت (`AXCommentItemTemplateContext`):
214
+
215
+ | فیلد | معنی |
216
+ | --- | --- |
217
+ | `$implicit` / `item` | خود `AXCommentItem` |
218
+ | `level` | عمق (۰ = ریشه) |
219
+ | `repliesExpanded` | باز بودن پاسخ‌ها |
220
+ | `replyCount` | تعداد پاسخ |
221
+ | `loading` | در حال لود lazy |
222
+ | `liked` / `likes` | وضعیت لایک |
223
+ | `toggleReplies()` | باز/بسته کردن |
224
+ | `toggleLike()` | لایک |
225
+ | `reply(event?)` | شروع ریپلای |
226
+
227
+ ---
228
+
229
+ ## ۴) Composer: ارسال، ریپلای، ویرایش
230
+
231
+ `ax-comment-reply` ادیتور را پروجکت می‌کند. اگر `item` ست باشد نوار «Replying to [نام]» با excerpt متن‌ساده بالای ادیتور می‌آید. کلیک روی نوار به همان کامنت اسکرول و highlight می‌کند. X فقط target را می‌بندد؛ پیش‌نویس پاک نمی‌شود.
232
+
233
+ ### Reply
234
+
235
+ ```ts
236
+ replyingTo = signal<AXCommentItem | null>(null);
237
+
238
+ onReply(event: AXCommentReplyEvent) {
239
+ this.replyingTo.set(event.item);
240
+ setTimeout(() => this.editor()?.focus());
241
+ }
242
+
243
+ cancelReply() {
244
+ this.replyingTo.set(null); // draft باقی می‌ماند
245
+ }
246
+ ```
247
+
248
+ عوض کردن هدف ریپلای draft را نگه می‌دارد.
249
+
250
+ ### Send
251
+
252
+ ```ts
253
+ send() {
254
+ const html = this.draft();
255
+ if (this.isEmpty(html)) return; // fail: draft و target می‌مانند
256
+
257
+ const item: AXCommentItem = {
258
+ id: crypto.randomUUID(),
259
+ author: 'You',
260
+ initials: 'Y',
261
+ isYou: true,
262
+ date: 'Just now',
263
+ content: html,
264
+ likes: 0,
265
+ actions,
266
+ };
267
+
268
+ const parent = this.replyingTo();
269
+ const view = this.commentsView();
270
+
271
+ if (parent) {
272
+ const parentId = String(parent.id);
273
+ if (view && !view.findItem(parentId)) return;
274
+ void view?.appendReply(parentId, item);
275
+ this.replyingTo.set(null);
276
+ } else {
277
+ view?.appendItem(item);
278
+ }
279
+
280
+ this.draft.set('');
281
+ this.editor()?.setContent('', false);
282
+ }
283
+ ```
284
+
285
+ `ax-text-editor` برای ارتفاع از `[initialLine]="3"` استفاده می‌کند. برای ست کردن محتوا و فوکوس:
286
+
287
+ ```ts
288
+ this.editor()?.setContent(html, true);
289
+ ```
290
+
291
+ ### Edit / Delete (از منوی ...)
292
+
293
+ ```ts
294
+ onAction(event: AXCommentActionEvent) {
295
+ if (event.action.name === 'edit') this.edit(event.item);
296
+ if (event.action.name === 'delete') void this.remove(event.item);
297
+ }
298
+
299
+ edit(item: AXCommentItem) {
300
+ this.replyingTo.set(null);
301
+ this.editing.set(item);
302
+ this.editor()?.setContent(item.content ?? '', true);
303
+ }
304
+
305
+ saveEdit() {
306
+ const html = this.draft();
307
+ const current = this.editing();
308
+ if (!current || this.isEmpty(html)) return;
309
+ const updated = this.commentsView()?.updateItem(String(current.id), { content: html, edited: true });
310
+ if (!updated) return;
311
+ this.editing.set(null);
312
+ this.draft.set('');
313
+ }
314
+
315
+ async remove(item: AXCommentItem) {
316
+ const { result } = await this.dialog.confirm(
317
+ 'Delete comment',
318
+ 'This comment will be permanently removed.',
319
+ 'danger',
320
+ );
321
+ if (!result) return;
322
+ this.commentsView()?.removeItem(String(item.id));
323
+ }
324
+ ```
325
+
326
+ اگر datasource تابع lazy است، بعد از `appendItem` / `removeItem` / `updateItem` منبع خودتان (mock API / سیگنال) را هم همگام کنید تا reload بعدی درست باشد.
327
+
328
+ ---
329
+
330
+ ## ۵) APIی `ax-comment-view`
331
+
332
+ ### Inputs / model
333
+
334
+ | نام | نوع | توضیح |
335
+ | --- | --- | --- |
336
+ | `datasource` | `AXCommentItem[] \| AXCommentCallback \| null` | آرایه یا callback تنبل |
337
+ | `itemTemplate` | `TemplateRef<AXCommentItemTemplateContext>` | جایگزین `ng-template axCommentItem` |
338
+
339
+ ### Events
340
+
341
+ | نام | payload | کی |
342
+ | --- | --- | --- |
343
+ | `onReply` | `AXCommentReplyEvent` | کلیک Reply |
344
+ | `onLikeChange` | `AXCommentLikeChangeEvent` | تغییر لایک (view خودش liked/likes را آپدیت می‌کند) |
345
+ | `onRepliesToggle` | `AXCommentRepliesToggleEvent` | باز/بسته شدن پاسخ‌ها |
346
+ | `onAction` | `AXCommentActionEvent` | انتخاب آیتم منوی `...` — `event.action` از نوع `AXMenuItem` |
347
+
348
+ ### Methods
349
+
350
+ | متد | کار |
351
+ | --- | --- |
352
+ | `reloadData()` | ریشه‌ها را دوباره از datasource می‌گیرد |
353
+ | `appendItem(item)` | کامنت ریشه اضافه می‌کند |
354
+ | `appendReply(parentId, item)` | پاسخ می‌گذارد؛ اگر لازم باشد اول lazy load می‌کند و والد را باز می‌کند |
355
+ | `findItem(id)` | جستجو در درخت لودشده |
356
+ | `updateItem(id, patch)` | پچ آیتم؛ `id` اصلی حفظ می‌شود |
357
+ | `removeItem(id)` | حذف آیتم و پاسخ‌های تو در تو |
358
+ | `setExpanded(id, expanded)` | باز/بسته + lazy load |
359
+
360
+ ```ts
361
+ const view = viewChild('commentsView', { read: AXCommentViewComponent });
362
+ ```
363
+
364
+ ---
365
+
366
+ ## ۶) `ax-comment-reply`
367
+
368
+ | | |
369
+ | --- | --- |
370
+ | `[item]` | `AXCommentItem \| null` — نوار فقط وقتی مقدار دارد دیده می‌شود |
371
+ | `(onClose)` | کلیک X |
372
+ | `(onTargetClick)` | کلیک روی نوار؛ خود کامپوننت هم اسکرول/highlight می‌کند |
373
+
374
+ Excerpt HTML را strip می‌کند و متن ساده نشان می‌دهد.
375
+
376
+ ---
377
+
378
+ ## ۷) `ax-comment-container`
379
+
380
+ اسلات‌ها: `ax-header`، محتوا، `ax-footer`.
381
+
382
+ ```ts
383
+ scrollToReply(id: string): void
384
+ ```
385
+
386
+ هر دو سلکتور را امتحان می‌کند: `ax-comment-item[id="…"]` و `comment-${id}`. view برای آیتم‌ها `id="comment-${id}"` می‌گذارد.
387
+
388
+ ---
389
+
390
+ ## ۸) حالت declarative (بدون datasource)
391
+
392
+ ```html
393
+ <ax-comment-view>
394
+ <ax-comment-item [replyCount]="1" [(repliesExpanded)]="open">
395
+ <ax-avatar color="success" look="twotone" shape="rounded">
396
+ <ax-text>H</ax-text>
397
+ </ax-avatar>
398
+ <ax-title>Heidi Thonig</ax-title>
399
+ <ax-comment-date>18 hours ago</ax-comment-date>
400
+ <ax-content>Hello</ax-content>
401
+ <ax-comment-like [like]="true">1</ax-comment-like>
402
+ <ax-comment-reply-text (click)="onReply($event)" />
403
+
404
+ <ax-comment-item>
405
+ <ax-avatar color="primary" look="twotone" shape="rounded">
406
+ <ax-text>A</ax-text>
407
+ </ax-avatar>
408
+ <ax-title>Alex</ax-title>
409
+ <ax-content>Hi</ax-content>
410
+ <ax-comment-like [like]="false">0</ax-comment-like>
411
+ <ax-comment-reply-text />
412
+ </ax-comment-item>
413
+ </ax-comment-item>
414
+ </ax-comment-view>
415
+ ```
416
+
417
+ ---
418
+
419
+ ## ۹) i18n
420
+
421
+ کلیدها در `@acorex/core` (`comment.*`):
422
+
423
+ | کلید | en-US |
424
+ | --- | --- |
425
+ | `comment.reply` | Reply |
426
+ | `comment.replies` | Replies |
427
+ | `comment.replying-to` | Replying to |
428
+ | `comment.you` | You |
429
+ | `comment.edited` | Edited |
430
+ | `comment.view` | View |
431
+ | `comment.hide` | Hide |
432
+
433
+ ---
434
+
435
+ ## ۱۰) توکن‌های CSS
436
+
437
+ روی `ax-comment-container`:
438
+
439
+ | توکن | پیش‌فرض |
440
+ | --- | --- |
441
+ | `--ax-comp-comment-padding-y` | `8px` |
442
+ | `--ax-comp-comment-gap` | `8px` |
443
+ | `--ax-comp-comment-avatar-size` | `32px` |
444
+ | `--ax-comp-comment-reply-avatar-size` | `28px` |
445
+
446
+ لی‌آوت پیش‌فرض آیتم avatar را `48px` می‌گذارد. فاصله داخلی آیتم از `--ax-comp-comment-gap` (پیش‌فرض `0.5rem` روی خود آیتم) است.
447
+
448
+ نوار ریپلای:
449
+
450
+ | توکن | نقش |
451
+ | --- | --- |
452
+ | `--ax-comp-comment-reply-bar-bg-color` | پس‌زمینه |
453
+ | `--ax-comp-comment-reply-bar-border-color` | حاشیه |
454
+ | `--ax-comp-comment-reply-bar-accent-color` | نوار تأکیدی اینلاین‌استارت |
455
+ | `--ax-comp-comment-reply-bar-radius` | `6px` |
456
+ | `--ax-comp-comment-reply-bar-gap` | فاصله تا ادیتور (`8px`) |
457
+
458
+ Highlight اسکرول: کلاس `ax-state-highlight` روی `ax-comment-item`.
459
+
460
+ ---
461
+
462
+ ## نکات
463
+
464
+ - `content` می‌تواند HTML باشد (`<ul>`، mention و …). نوار ریپلای آن را به متن ساده تبدیل می‌کند.
465
+ - منوی `...` فقط UI پیش‌فرض است، نه تمپلیت سفارشی.
466
+ - روی دستگاه‌های touch، سه نقطه همیشه دیده می‌شود.
467
+ - `appendReply` / `updateItem` / `removeItem` درخت **لودشده** را عوض می‌کنند؛ منبع lazy را خودتان هم به‌روز کنید.
468
+ - ارتفاع ادیتور: `[initialLine]="n"` روی `ax-text-editor`.