pukaad-ui-lib 1.201.0 → 1.203.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.
Files changed (36) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/comment.d.vue.ts +12 -0
  3. package/dist/runtime/components/comment.vue +312 -85
  4. package/dist/runtime/components/comment.vue.d.ts +12 -0
  5. package/dist/runtime/components/modal/modal-report.d.vue.ts +22 -1
  6. package/dist/runtime/components/modal/modal-report.vue +86 -26
  7. package/dist/runtime/components/modal/modal-report.vue.d.ts +22 -1
  8. package/dist/runtime/components/picker/picker-option-menu/picker-option-menu-user.vue +2 -1
  9. package/dist/runtime/components/picker/picker-option-menu/picker-option-menu.d.vue.ts +11 -2
  10. package/dist/runtime/components/picker/picker-option-menu/picker-option-menu.vue +8 -4
  11. package/dist/runtime/components/picker/picker-option-menu/picker-option-menu.vue.d.ts +11 -2
  12. package/dist/runtime/components/ui/button/index.d.ts +1 -1
  13. package/dist/runtime/components/ui/button/index.js +1 -0
  14. package/dist/runtime/components/ui/carousel/CarouselNext.d.vue.ts +1 -1
  15. package/dist/runtime/components/ui/carousel/CarouselNext.vue.d.ts +1 -1
  16. package/dist/runtime/components/ui/carousel/CarouselPrevious.d.vue.ts +1 -1
  17. package/dist/runtime/components/ui/carousel/CarouselPrevious.vue.d.ts +1 -1
  18. package/dist/runtime/components/ui/input-group/InputGroupButton.d.vue.ts +1 -1
  19. package/dist/runtime/components/ui/input-group/InputGroupButton.vue.d.ts +1 -1
  20. package/dist/runtime/components/ui/input-group/index.d.ts +1 -1
  21. package/dist/runtime/components/ui/pagination/PaginationFirst.d.vue.ts +1 -1
  22. package/dist/runtime/components/ui/pagination/PaginationFirst.vue.d.ts +1 -1
  23. package/dist/runtime/components/ui/pagination/PaginationItem.d.vue.ts +1 -1
  24. package/dist/runtime/components/ui/pagination/PaginationItem.vue.d.ts +1 -1
  25. package/dist/runtime/components/ui/pagination/PaginationLast.d.vue.ts +1 -1
  26. package/dist/runtime/components/ui/pagination/PaginationLast.vue.d.ts +1 -1
  27. package/dist/runtime/components/ui/pagination/PaginationNext.d.vue.ts +1 -1
  28. package/dist/runtime/components/ui/pagination/PaginationNext.vue.d.ts +1 -1
  29. package/dist/runtime/components/ui/pagination/PaginationPrevious.d.vue.ts +1 -1
  30. package/dist/runtime/components/ui/pagination/PaginationPrevious.vue.d.ts +1 -1
  31. package/dist/runtime/components/ui/select/Select.d.vue.ts +2 -2
  32. package/dist/runtime/components/ui/select/Select.vue.d.ts +2 -2
  33. package/package.json +1 -1
  34. package/dist/runtime/components/comments/CommentSection.d.vue.ts +0 -49
  35. package/dist/runtime/components/comments/CommentSection.vue +0 -360
  36. package/dist/runtime/components/comments/CommentSection.vue.d.ts +0 -49
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
3
  "configKey": "pukaadUI",
4
- "version": "1.201.0",
4
+ "version": "1.203.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -32,11 +32,23 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
32
32
  reply: (commentId: string, replyToUserId: string | undefined, content: string) => any;
33
33
  "like-comment": (comment: CommentItem) => any;
34
34
  "like-reply": (reply: Reply) => any;
35
+ "edit-comment": (comment: CommentItem, content: string) => any;
36
+ "edit-reply": (reply: Reply, content: string) => any;
37
+ "delete-comment": (comment: CommentItem) => any;
38
+ "delete-reply": (reply: Reply) => any;
39
+ "report-comment": (comment: CommentItem) => any;
40
+ "report-reply": (reply: Reply) => any;
35
41
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
36
42
  onSubmit?: ((content: string) => any) | undefined;
37
43
  onReply?: ((commentId: string, replyToUserId: string | undefined, content: string) => any) | undefined;
38
44
  "onLike-comment"?: ((comment: CommentItem) => any) | undefined;
39
45
  "onLike-reply"?: ((reply: Reply) => any) | undefined;
46
+ "onEdit-comment"?: ((comment: CommentItem, content: string) => any) | undefined;
47
+ "onEdit-reply"?: ((reply: Reply, content: string) => any) | undefined;
48
+ "onDelete-comment"?: ((comment: CommentItem) => any) | undefined;
49
+ "onDelete-reply"?: ((reply: Reply) => any) | undefined;
50
+ "onReport-comment"?: ((comment: CommentItem) => any) | undefined;
51
+ "onReport-reply"?: ((reply: Reply) => any) | undefined;
40
52
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
41
53
  declare const _default: typeof __VLS_export;
42
54
  export default _default;
@@ -5,7 +5,7 @@
5
5
  </div>
6
6
  <div class="flex flex-col gap-[16px]">
7
7
  <div class="flex gap-[8px] w-full">
8
- <Avatar :size="30" />
8
+ <Avatar :size="30" :src="currentUser.avatar" />
9
9
  <div
10
10
  ref="mainInputRef"
11
11
  contenteditable="true"
@@ -37,40 +37,93 @@
37
37
  class="flex flex-col gap-[16px] w-full"
38
38
  >
39
39
  <div class="flex flex-col gap-[4px] w-full">
40
- <div class="flex gap-[8px]">
40
+ <div
41
+ class="flex gap-[8px]"
42
+ @mouseenter="hoveredId = cmt.id"
43
+ @mouseleave="hoveredId = null"
44
+ >
41
45
  <Avatar
42
46
  :size="30"
47
+ :src="cmt.user.avatar"
43
48
  class="cursor-pointer"
44
49
  @click="onViewProfileComment(cmt.user.id)"
45
50
  />
46
- <div class="flex flex-col gap-[4px] bg-bright p-[8px] rounded-lg">
47
- <div
48
- class="font-body-large-prominent cursor-pointer"
49
- @click="onViewProfileComment(cmt.user.id)"
50
- >
51
- {{ cmt.user.name }}
51
+ <template v-if="editingId === cmt.id">
52
+ <div class="flex-1 flex flex-col gap-[8px]">
53
+ <div
54
+ :ref="(el) => setEditInputRef(el, cmt.id)"
55
+ contenteditable="true"
56
+ class="w-full min-h-[36px] px-[12px] py-[5px] rounded-md border border-mercury font-body-large focus:outline-none focus:ring-2 focus:ring-primary break-words bg-white"
57
+ @input="onEditInput($event, cmt.id)"
58
+ @keydown.enter.exact.prevent="onSendEdit(cmt, 'comment')"
59
+ @keydown.shift.enter.prevent="onInsertLineBreak"
60
+ />
61
+ <div class="flex justify-end gap-[8px]">
62
+ <Button
63
+ variant="outline"
64
+ class="w-[56px]"
65
+ @click="onCancelEdit"
66
+ >
67
+ ยกเลิก
68
+ </Button>
69
+ <Button
70
+ color="primary"
71
+ class="w-[56px]"
72
+ :disabled="!editTextMap[cmt.id]"
73
+ @click="onSendEdit(cmt, 'comment')"
74
+ >
75
+ ส่ง
76
+ </Button>
77
+ </div>
52
78
  </div>
53
- <div
54
- :ref="
79
+ </template>
80
+ <template v-else>
81
+ <div class="flex flex-col gap-[4px] bg-bright p-[8px] rounded-lg">
82
+ <div
83
+ class="font-body-large-prominent cursor-pointer"
84
+ @click="onViewProfileComment(cmt.user.id)"
85
+ >
86
+ {{ cmt.user.name }}
87
+ </div>
88
+ <div
89
+ :ref="
55
90
  (el) => registerContentRef(el, `comment-${cmt.id}`)
56
91
  "
57
- class="font-body-large whitespace-pre-wrap"
58
- >
59
- <template
60
- v-if="
92
+ class="font-body-large whitespace-pre-wrap"
93
+ >
94
+ <template
95
+ v-if="
61
96
  isContentExpanded(`comment-${cmt.id}`) || !isContentOverflowing(`comment-${cmt.id}`)
62
97
  "
63
- >{{ normalizeContent(cmt.content) }}</template
64
- >
65
- <template v-else
66
- >{{ getTruncatedText(`comment-${cmt.id}`) }}...<span
67
- class="text-primary cursor-pointer"
68
- @click="toggleContentExpand(`comment-${cmt.id}`)"
69
- >ดูเพิ่มเติม</span
70
- ></template
71
- >
98
+ >{{ normalizeContent(cmt.content) }}</template
99
+ >
100
+ <template v-else
101
+ >{{ getTruncatedText(`comment-${cmt.id}`) }}...<span
102
+ class="text-primary cursor-pointer"
103
+ @click="toggleContentExpand(`comment-${cmt.id}`)"
104
+ >ดูเพิ่มเติม</span
105
+ ></template
106
+ >
107
+ </div>
72
108
  </div>
73
- </div>
109
+ <div
110
+ class="self-center transition-opacity"
111
+ :class="
112
+ isMenuVisible(cmt.id) ? 'opacity-100' : 'opacity-0 pointer-events-none'
113
+ "
114
+ >
115
+ <PickerOptionMenu
116
+ horizontal
117
+ variant="ghost"
118
+ circle
119
+ size="icon-sm"
120
+ icon-size="20"
121
+ :items="getCommentMenuItems(cmt)"
122
+ :open="openMenuId === cmt.id"
123
+ @update:open="(v) => openMenuId = v ? cmt.id : null"
124
+ />
125
+ </div>
126
+ </template>
74
127
  </div>
75
128
  <div class="pl-[38px] flex gap-[16px] text-gray">
76
129
  <div class="font-body-medium">
@@ -86,7 +139,7 @@
86
139
  "
87
140
  :class="[cmt.is_liked && 'text-primary']"
88
141
  />
89
- <div :class="[cmt.is_liked && 'text-primary']">
142
+ <div v-if="cmt.like_count > 0" :class="[cmt.is_liked && 'text-primary']">
90
143
  {{ $convert.convertNumber(cmt.like_count) }}
91
144
  </div>
92
145
  </div>
@@ -98,17 +151,17 @@
98
151
  </div>
99
152
  </div>
100
153
  <div
101
- v-if="hasOtherUserReplies(cmt)"
154
+ v-if="getHiddenCount(cmt) > 0"
102
155
  class="pl-[38px] flex flex-col gap-[16px]"
103
156
  >
104
157
  <Button
105
158
  variant="text"
106
- class="justify-start text-primary p-0 h-auto gap-[4px]"
159
+ class="justify-start text-primary h-auto gap-[4px] w-fit"
107
160
  @click="onToggleReplyComment(cmt.id)"
108
161
  >
109
162
  <div class="font-body-large-prominent">
110
163
  การตอบกลับ
111
- {{ $convert.convertNumber(getOtherUserRepliesCount(cmt)) }}
164
+ {{ $convert.convertNumber(getHiddenCount(cmt)) }}
112
165
  รายการ
113
166
  </div>
114
167
  <Icon
@@ -121,7 +174,7 @@
121
174
  </div>
122
175
  <div v-if="replyingToId === cmt.id" class="flex flex-col gap-[16px]">
123
176
  <div class="flex gap-[8px] w-full">
124
- <Avatar :size="30" />
177
+ <Avatar :size="30" :src="currentUser.avatar" />
125
178
  <div class="flex-1">
126
179
  <div
127
180
  :ref="(el) => setReplyInputRef(el, cmt.id)"
@@ -153,63 +206,119 @@
153
206
  </div>
154
207
  </div>
155
208
  <div
156
- v-if="openedComments.includes(cmt.id) || hasCurrentUserReplies(cmt)"
157
- class="pl-[38px] flex flex-col gap-[16px] w-full"
209
+ v-if="cmt.reply.length > 0"
210
+ v-show="openedComments.includes(cmt.id) || cmt.reply.some((r) => pinnedReplyIds.has(r.id))"
211
+ class="pl-[38px] flex flex-col gap-[16px]"
158
212
  >
159
213
  <div
160
214
  v-for="reply in cmt.reply"
161
- v-show="
162
- openedComments.includes(cmt.id) || reply.user.id === currentUser.id
163
- "
215
+ v-show="isReplyVisible(cmt, reply)"
164
216
  :key="reply.id"
165
- :data-reply-id="reply.id"
166
217
  class="flex flex-col gap-[16px]"
167
218
  >
168
- <div class="flex flex-col gap-[4px] w-full">
169
- <div class="flex gap-[8px]">
219
+ <div class="flex flex-col gap-[4px]">
220
+ <div
221
+ class="flex gap-[8px]"
222
+ @mouseenter="hoveredId = reply.id"
223
+ @mouseleave="hoveredId = null"
224
+ >
170
225
  <Avatar
171
226
  :size="30"
227
+ :src="reply.user.avatar"
172
228
  class="cursor-pointer"
173
229
  @click="onViewProfileComment(reply.user.id)"
174
230
  />
175
- <div class="flex flex-col bg-bright p-[8px] rounded-lg gap-[4px]">
231
+ <template v-if="editingId === reply.id">
232
+ <div class="flex-1 flex flex-col gap-[8px]">
233
+ <div
234
+ :ref="(el) => setEditInputRef(el, reply.id)"
235
+ contenteditable="true"
236
+ class="w-full min-h-[36px] px-[12px] py-[5px] rounded-md border border-mercury font-body-large focus:outline-none focus:ring-2 focus:ring-primary break-words bg-white"
237
+ @input="onEditInput($event, reply.id)"
238
+ @keydown.enter.exact.prevent="onSendEdit(reply, 'reply')"
239
+ @keydown.shift.enter.prevent="onInsertLineBreak"
240
+ />
241
+ <div class="flex justify-end gap-[8px]">
242
+ <Button
243
+ variant="outline"
244
+ class="w-[56px]"
245
+ @click="onCancelEdit"
246
+ >
247
+ ยกเลิก
248
+ </Button>
249
+ <Button
250
+ color="primary"
251
+ class="w-[56px]"
252
+ :disabled="!editTextMap[reply.id]"
253
+ @click="onSendEdit(reply, 'reply')"
254
+ >
255
+ ส่ง
256
+ </Button>
257
+ </div>
258
+ </div>
259
+ </template>
260
+ <template v-else>
176
261
  <div
177
- class="font-body-large-prominent cursor-pointer"
178
- @click="onViewProfileComment(reply.user.id)"
262
+ class="flex flex-col bg-bright p-[8px] rounded-lg gap-[4px]"
179
263
  >
180
- {{ reply.user.name }}
264
+ <div
265
+ class="font-body-large-prominent cursor-pointer"
266
+ @click="onViewProfileComment(reply.user.id)"
267
+ >
268
+ {{ reply.user.name }}
269
+ </div>
270
+ <div
271
+ :ref="
272
+ (el) => registerContentRef(
273
+ el,
274
+ `reply-${reply.id}`
275
+ )
276
+ "
277
+ class="font-body-large whitespace-pre-wrap"
278
+ >
279
+ <template
280
+ v-if="
281
+ isContentExpanded(`reply-${reply.id}`) || !isContentOverflowing(`reply-${reply.id}`)
282
+ "
283
+ ><span
284
+ v-if="reply.reply_to"
285
+ @click="onViewProfileComment(reply.reply_to.id)"
286
+ class="text-primary cursor-pointer mr-1"
287
+ >{{ reply.reply_to.name }}</span
288
+ >{{ normalizeContent(reply.content) }}</template
289
+ >
290
+ <template v-else
291
+ ><span
292
+ v-if="reply.reply_to"
293
+ @click="onViewProfileComment(reply.reply_to.id)"
294
+ class="text-primary cursor-pointer mr-1"
295
+ >{{ reply.reply_to.name }}</span
296
+ >{{ getTruncatedText(`reply-${reply.id}`) }}...<span
297
+ class="text-primary cursor-pointer"
298
+ @click="toggleContentExpand(`reply-${reply.id}`)"
299
+ >ดูเพิ่มเติม</span
300
+ ></template
301
+ >
302
+ </div>
181
303
  </div>
182
304
  <div
183
- :ref="
184
- (el) => registerContentRef(el, `reply-${reply.id}`)
305
+ class="self-center transition-opacity"
306
+ :class="
307
+ isMenuVisible(reply.id) ? 'opacity-100' : 'opacity-0 pointer-events-none'
185
308
  "
186
- class="font-body-large whitespace-pre-wrap"
187
309
  >
188
- <template
189
- v-if="
190
- isContentExpanded(`reply-${reply.id}`) || !isContentOverflowing(`reply-${reply.id}`)
191
- "
192
- ><span
193
- v-if="reply.reply_to"
194
- @click="onViewProfileComment(reply.reply_to.id)"
195
- class="text-primary cursor-pointer mr-1"
196
- >{{ reply.reply_to.name }}</span
197
- >{{ normalizeContent(reply.content) }}</template
198
- >
199
- <template v-else
200
- ><span
201
- v-if="reply.reply_to"
202
- @click="onViewProfileComment(reply.reply_to.id)"
203
- class="text-primary cursor-pointer mr-1"
204
- >{{ reply.reply_to.name }}</span
205
- >{{ getTruncatedText(`reply-${reply.id}`) }}...<span
206
- class="text-primary cursor-pointer"
207
- @click="toggleContentExpand(`reply-${reply.id}`)"
208
- >ดูเพิ่มเติม</span
209
- ></template
210
- >
310
+ <PickerOptionMenu
311
+ horizontal
312
+ variant="ghost"
313
+ circle
314
+ size="icon-xs"
315
+ icon-size="20"
316
+ :items="getReplyMenuItems(reply)"
317
+ :open="openMenuId === reply.id"
318
+ @update:open="(v) => openMenuId = v ? reply.id : null"
319
+ />
211
320
  </div>
212
- </div>
321
+ </template>
213
322
  </div>
214
323
  <div class="pl-[38px] flex gap-[16px] text-gray">
215
324
  <div class="font-body-medium">
@@ -225,7 +334,7 @@
225
334
  "
226
335
  :class="[reply.is_liked && 'text-primary']"
227
336
  />
228
- <div :class="[reply.is_liked && 'text-primary']">
337
+ <div v-if="reply.like_count > 0" :class="[reply.is_liked && 'text-primary']">
229
338
  {{ $convert.convertNumber(reply.like_count) }}
230
339
  </div>
231
340
  </div>
@@ -242,12 +351,12 @@
242
351
  class="flex flex-col gap-[16px]"
243
352
  >
244
353
  <div class="flex gap-[8px] w-full">
245
- <Avatar :size="30" />
354
+ <Avatar :size="30" :src="currentUser.avatar" />
246
355
  <div class="flex-1">
247
356
  <div
248
357
  :ref="(el) => setReplyInputRef(el, reply.id)"
249
358
  contenteditable="true"
250
- class="w-full min-h-[40px] px-[12px] py-[8px] rounded-lg border border-gray-300 font-body-large focus:outline-none focus:ring-2 focus:ring-primary break-words bg-white empty:before:content-[attr(data-placeholder)] empty:before:text-gray-400"
359
+ class="w-full min-h-[36px] px-[12px] py-[5px] rounded-md border border-mercury font-body-large focus:outline-none focus:ring-2 focus:ring-primary break-words bg-white empty:before:content-[attr(data-placeholder)] empty:before:text-gray"
251
360
  data-placeholder="เขียนการตอบกลับ..."
252
361
  @input="onReplyInput($event, reply.id)"
253
362
  @keydown.enter.exact.prevent="onSendReplyComment(cmt)"
@@ -280,7 +389,7 @@
280
389
  </template>
281
390
 
282
391
  <script setup>
283
- import { ref, nextTick, computed } from "vue";
392
+ import { ref, nextTick, watch } from "vue";
284
393
  import { useRouter } from "vue-router";
285
394
  import { useConvert } from "@/runtime/composables/useConvert";
286
395
  const props = defineProps({
@@ -288,10 +397,9 @@ const props = defineProps({
288
397
  commentCount: { type: Number, required: true },
289
398
  currentUser: { type: Object, required: true }
290
399
  });
291
- const emit = defineEmits(["submit", "reply", "like-comment", "like-reply"]);
400
+ const emit = defineEmits(["submit", "reply", "like-comment", "like-reply", "edit-comment", "edit-reply", "delete-comment", "delete-reply", "report-comment", "report-reply"]);
292
401
  const router = useRouter();
293
402
  const { convertDateTorelativeText } = useConvert();
294
- const currentUser = computed(() => props.currentUser);
295
403
  const openedComments = ref([]);
296
404
  const expandedContentIds = ref([]);
297
405
  const truncatedTexts = ref({});
@@ -302,6 +410,48 @@ const replyingToId = ref(null);
302
410
  const replyToUser = ref(null);
303
411
  const replyInputRefs = ref({});
304
412
  const replyTextMap = ref({});
413
+ const pinnedReplyIds = ref(/* @__PURE__ */ new Set());
414
+ let knownReplyIds = null;
415
+ watch(
416
+ () => props.comments.flatMap((c) => c.reply.map((r) => r.id)),
417
+ (allIds) => {
418
+ if (knownReplyIds === null) {
419
+ if (allIds.length === 0) return;
420
+ knownReplyIds = new Set(allIds);
421
+ return;
422
+ }
423
+ const liveIds = new Set(allIds);
424
+ allIds.forEach((id) => {
425
+ if (knownReplyIds.has(id)) return;
426
+ knownReplyIds.add(id);
427
+ pinnedReplyIds.value.add(id);
428
+ });
429
+ knownReplyIds.forEach((id) => {
430
+ if (!liveIds.has(id)) {
431
+ knownReplyIds.delete(id);
432
+ pinnedReplyIds.value.delete(id);
433
+ }
434
+ });
435
+ },
436
+ { immediate: true }
437
+ );
438
+ const getHiddenCount = (cmt) => cmt.reply.filter((r) => !pinnedReplyIds.value.has(r.id)).length;
439
+ const isReplyVisible = (cmt, reply) => openedComments.value.includes(cmt.id) || pinnedReplyIds.value.has(reply.id);
440
+ const hoveredId = ref(null);
441
+ const openMenuId = ref(null);
442
+ const isMenuVisible = (id) => hoveredId.value === id || openMenuId.value === id;
443
+ const editingId = ref(null);
444
+ const editInputRefs = ref({});
445
+ const editTextMap = ref({});
446
+ const isSelfUser = (user) => !!props.currentUser.id && user.id === props.currentUser.id;
447
+ const buildMentionChip = (name) => {
448
+ const chip = document.createElement("span");
449
+ chip.dataset.type = "mention";
450
+ chip.contentEditable = "false";
451
+ chip.className = "inline-flex items-center text-primary font-body-large-prominent select-none mr-1";
452
+ chip.textContent = name;
453
+ return chip;
454
+ };
305
455
  const onToggleReplyComment = (id) => {
306
456
  if (openedComments.value.includes(id)) {
307
457
  openedComments.value = openedComments.value.filter((i) => i !== id);
@@ -396,21 +546,17 @@ const setReplyInputRef = (el, id) => {
396
546
  else delete replyInputRefs.value[id];
397
547
  };
398
548
  const onReplyComment = (id, user) => {
549
+ const isSelf = isSelfUser(user);
399
550
  replyingToId.value = id;
400
- replyToUser.value = user.id !== currentUser.value.id ? user : null;
551
+ replyToUser.value = isSelf ? null : user;
401
552
  replyTextMap.value[id] = "";
402
553
  nextTick(() => {
403
554
  const el = replyInputRefs.value[id];
404
555
  if (!el) return;
405
556
  el.innerHTML = "";
406
- if (user.id !== currentUser.value.id) {
407
- const chip = document.createElement("span");
408
- chip.dataset.type = "mention";
409
- chip.contentEditable = "false";
410
- chip.className = "inline-flex items-center text-primary font-body-large-prominent select-none mr-1";
411
- chip.textContent = user.name;
557
+ if (!isSelf) {
412
558
  const space = document.createTextNode("\xA0");
413
- el.appendChild(chip);
559
+ el.appendChild(buildMentionChip(user.name));
414
560
  el.appendChild(space);
415
561
  const range = document.createRange();
416
562
  const sel = window.getSelection();
@@ -498,7 +644,88 @@ const onToggleLikeReplyComment = (reply) => {
498
644
  const onViewProfileComment = (id) => {
499
645
  if (id) router.push(`/@${id}`);
500
646
  };
501
- const getOtherUserRepliesCount = (cmt) => (cmt.reply || []).filter((r) => r.user?.id !== currentUser.value?.id).length;
502
- const hasOtherUserReplies = (cmt) => getOtherUserRepliesCount(cmt) > 0;
503
- const hasCurrentUserReplies = (cmt) => (cmt.reply || []).some((r) => r.user?.id === currentUser.value?.id);
647
+ const setEditInputRef = (el, id) => {
648
+ if (el) editInputRefs.value[id] = el;
649
+ else delete editInputRefs.value[id];
650
+ };
651
+ const onEditClick = (id, content, replyTo) => {
652
+ editingId.value = id;
653
+ editTextMap.value[id] = content;
654
+ nextTick(() => {
655
+ const el = editInputRefs.value[id];
656
+ if (!el) return;
657
+ el.innerHTML = "";
658
+ if (replyTo) {
659
+ el.appendChild(buildMentionChip(replyTo.name));
660
+ el.appendChild(document.createTextNode("\xA0"));
661
+ }
662
+ el.appendChild(document.createTextNode(content));
663
+ const range = document.createRange();
664
+ range.selectNodeContents(el);
665
+ range.collapse(false);
666
+ const sel = window.getSelection();
667
+ sel?.removeAllRanges();
668
+ sel?.addRange(range);
669
+ el.focus();
670
+ });
671
+ };
672
+ const onEditInput = (event, id) => {
673
+ const el = event.target;
674
+ editTextMap.value[id] = readTextWithBreaks(el, true);
675
+ };
676
+ const onCancelEdit = () => {
677
+ const id = editingId.value;
678
+ if (id) delete editTextMap.value[id];
679
+ editingId.value = null;
680
+ };
681
+ const onSendEdit = (item, type) => {
682
+ const content = editTextMap.value[item.id]?.trim() ?? "";
683
+ if (!content) return;
684
+ if (type === "comment") emit("edit-comment", item, content);
685
+ else emit("edit-reply", item, content);
686
+ delete editTextMap.value[item.id];
687
+ editingId.value = null;
688
+ };
689
+ const getCommentMenuItems = (comment) => isSelfUser(comment.user) ? [
690
+ {
691
+ label: "\u0E41\u0E01\u0E49\u0E44\u0E02",
692
+ name: "edit",
693
+ icon: "lucide:pencil",
694
+ action: () => onEditClick(comment.id, comment.content)
695
+ },
696
+ {
697
+ label: "\u0E25\u0E1A",
698
+ name: "delete",
699
+ icon: "lucide:trash-2",
700
+ action: () => emit("delete-comment", comment)
701
+ }
702
+ ] : [
703
+ {
704
+ label: "\u0E23\u0E32\u0E22\u0E07\u0E32\u0E19",
705
+ name: "report",
706
+ icon: "lucide:flag",
707
+ action: () => emit("report-comment", comment)
708
+ }
709
+ ];
710
+ const getReplyMenuItems = (reply) => isSelfUser(reply.user) ? [
711
+ {
712
+ label: "\u0E41\u0E01\u0E49\u0E44\u0E02",
713
+ name: "edit",
714
+ icon: "lucide:pencil",
715
+ action: () => onEditClick(reply.id, reply.content, reply.reply_to)
716
+ },
717
+ {
718
+ label: "\u0E25\u0E1A",
719
+ name: "delete",
720
+ icon: "lucide:trash-2",
721
+ action: () => emit("delete-reply", reply)
722
+ }
723
+ ] : [
724
+ {
725
+ label: "\u0E23\u0E32\u0E22\u0E07\u0E32\u0E19",
726
+ name: "report",
727
+ icon: "lucide:flag",
728
+ action: () => emit("report-reply", reply)
729
+ }
730
+ ];
504
731
  </script>
@@ -32,11 +32,23 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
32
32
  reply: (commentId: string, replyToUserId: string | undefined, content: string) => any;
33
33
  "like-comment": (comment: CommentItem) => any;
34
34
  "like-reply": (reply: Reply) => any;
35
+ "edit-comment": (comment: CommentItem, content: string) => any;
36
+ "edit-reply": (reply: Reply, content: string) => any;
37
+ "delete-comment": (comment: CommentItem) => any;
38
+ "delete-reply": (reply: Reply) => any;
39
+ "report-comment": (comment: CommentItem) => any;
40
+ "report-reply": (reply: Reply) => any;
35
41
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
36
42
  onSubmit?: ((content: string) => any) | undefined;
37
43
  onReply?: ((commentId: string, replyToUserId: string | undefined, content: string) => any) | undefined;
38
44
  "onLike-comment"?: ((comment: CommentItem) => any) | undefined;
39
45
  "onLike-reply"?: ((reply: Reply) => any) | undefined;
46
+ "onEdit-comment"?: ((comment: CommentItem, content: string) => any) | undefined;
47
+ "onEdit-reply"?: ((reply: Reply, content: string) => any) | undefined;
48
+ "onDelete-comment"?: ((comment: CommentItem) => any) | undefined;
49
+ "onDelete-reply"?: ((reply: Reply) => any) | undefined;
50
+ "onReport-comment"?: ((comment: CommentItem) => any) | undefined;
51
+ "onReport-reply"?: ((reply: Reply) => any) | undefined;
40
52
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
41
53
  declare const _default: typeof __VLS_export;
42
54
  export default _default;
@@ -1,3 +1,24 @@
1
- declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
1
+ import type { ModalReportProps } from "@/types/components/modal/modal-report";
2
+ type __VLS_Props = ModalReportProps;
3
+ type __VLS_ModelProps = {
4
+ modelValue?: boolean;
5
+ };
6
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
7
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
+ "update:modelValue": (value: boolean) => any;
9
+ } & {
10
+ submit: (payload: {
11
+ value: string;
12
+ detail: string;
13
+ }) => any;
14
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
+ onSubmit?: ((payload: {
16
+ value: string;
17
+ detail: string;
18
+ }) => any) | undefined;
19
+ "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
20
+ }>, {
21
+ loading: boolean;
22
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
2
23
  declare const _default: typeof __VLS_export;
3
24
  export default _default;