pukaad-ui-lib 1.202.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 (47) 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 +307 -82
  4. package/dist/runtime/components/comment.vue.d.ts +12 -0
  5. package/dist/runtime/components/image/image-cropper.d.vue.ts +2 -2
  6. package/dist/runtime/components/image/image-cropper.vue.d.ts +2 -2
  7. package/dist/runtime/components/input/input-file.d.vue.ts +1 -1
  8. package/dist/runtime/components/input/input-file.vue.d.ts +1 -1
  9. package/dist/runtime/components/input/input-password.d.vue.ts +1 -1
  10. package/dist/runtime/components/input/input-password.vue.d.ts +1 -1
  11. package/dist/runtime/components/input/input-slider.d.vue.ts +1 -1
  12. package/dist/runtime/components/input/input-slider.vue.d.ts +1 -1
  13. package/dist/runtime/components/input/input-textarea.d.vue.ts +1 -1
  14. package/dist/runtime/components/input/input-textarea.vue.d.ts +1 -1
  15. package/dist/runtime/components/modal/modal-password-confirmed.d.vue.ts +1 -1
  16. package/dist/runtime/components/modal/modal-password-confirmed.vue.d.ts +1 -1
  17. package/dist/runtime/components/modal/modal-password-verify.d.vue.ts +1 -1
  18. package/dist/runtime/components/modal/modal-password-verify.vue.d.ts +1 -1
  19. package/dist/runtime/components/modal/modal-report.d.vue.ts +22 -1
  20. package/dist/runtime/components/modal/modal-report.vue +86 -26
  21. package/dist/runtime/components/modal/modal-report.vue.d.ts +22 -1
  22. package/dist/runtime/components/picker/picker-option-menu/picker-option-menu-user.vue +2 -1
  23. package/dist/runtime/components/picker/picker-option-menu/picker-option-menu.d.vue.ts +11 -2
  24. package/dist/runtime/components/picker/picker-option-menu/picker-option-menu.vue +8 -4
  25. package/dist/runtime/components/picker/picker-option-menu/picker-option-menu.vue.d.ts +11 -2
  26. package/dist/runtime/components/ui/button/index.d.ts +1 -1
  27. package/dist/runtime/components/ui/button/index.js +1 -0
  28. package/dist/runtime/components/ui/carousel/CarouselNext.d.vue.ts +1 -1
  29. package/dist/runtime/components/ui/carousel/CarouselNext.vue.d.ts +1 -1
  30. package/dist/runtime/components/ui/carousel/CarouselPrevious.d.vue.ts +1 -1
  31. package/dist/runtime/components/ui/carousel/CarouselPrevious.vue.d.ts +1 -1
  32. package/dist/runtime/components/ui/input-group/InputGroupButton.d.vue.ts +1 -1
  33. package/dist/runtime/components/ui/input-group/InputGroupButton.vue.d.ts +1 -1
  34. package/dist/runtime/components/ui/input-group/index.d.ts +1 -1
  35. package/dist/runtime/components/ui/pagination/PaginationFirst.d.vue.ts +1 -1
  36. package/dist/runtime/components/ui/pagination/PaginationFirst.vue.d.ts +1 -1
  37. package/dist/runtime/components/ui/pagination/PaginationItem.d.vue.ts +1 -1
  38. package/dist/runtime/components/ui/pagination/PaginationItem.vue.d.ts +1 -1
  39. package/dist/runtime/components/ui/pagination/PaginationLast.d.vue.ts +1 -1
  40. package/dist/runtime/components/ui/pagination/PaginationLast.vue.d.ts +1 -1
  41. package/dist/runtime/components/ui/pagination/PaginationNext.d.vue.ts +1 -1
  42. package/dist/runtime/components/ui/pagination/PaginationNext.vue.d.ts +1 -1
  43. package/dist/runtime/components/ui/pagination/PaginationPrevious.d.vue.ts +1 -1
  44. package/dist/runtime/components/ui/pagination/PaginationPrevious.vue.d.ts +1 -1
  45. package/dist/runtime/components/ui/select/Select.d.vue.ts +2 -2
  46. package/dist/runtime/components/ui/select/Select.vue.d.ts +2 -2
  47. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
3
  "configKey": "pukaadUI",
4
- "version": "1.202.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;
@@ -37,41 +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"
43
47
  :src="cmt.user.avatar"
44
48
  class="cursor-pointer"
45
49
  @click="onViewProfileComment(cmt.user.id)"
46
50
  />
47
- <div class="flex flex-col gap-[4px] bg-bright p-[8px] rounded-lg">
48
- <div
49
- class="font-body-large-prominent cursor-pointer"
50
- @click="onViewProfileComment(cmt.user.id)"
51
- >
52
- {{ 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>
53
78
  </div>
54
- <div
55
- :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="
56
90
  (el) => registerContentRef(el, `comment-${cmt.id}`)
57
91
  "
58
- class="font-body-large whitespace-pre-wrap"
59
- >
60
- <template
61
- v-if="
92
+ class="font-body-large whitespace-pre-wrap"
93
+ >
94
+ <template
95
+ v-if="
62
96
  isContentExpanded(`comment-${cmt.id}`) || !isContentOverflowing(`comment-${cmt.id}`)
63
97
  "
64
- >{{ normalizeContent(cmt.content) }}</template
65
- >
66
- <template v-else
67
- >{{ getTruncatedText(`comment-${cmt.id}`) }}...<span
68
- class="text-primary cursor-pointer"
69
- @click="toggleContentExpand(`comment-${cmt.id}`)"
70
- >ดูเพิ่มเติม</span
71
- ></template
72
- >
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>
73
108
  </div>
74
- </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>
75
127
  </div>
76
128
  <div class="pl-[38px] flex gap-[16px] text-gray">
77
129
  <div class="font-body-medium">
@@ -87,7 +139,7 @@
87
139
  "
88
140
  :class="[cmt.is_liked && 'text-primary']"
89
141
  />
90
- <div :class="[cmt.is_liked && 'text-primary']">
142
+ <div v-if="cmt.like_count > 0" :class="[cmt.is_liked && 'text-primary']">
91
143
  {{ $convert.convertNumber(cmt.like_count) }}
92
144
  </div>
93
145
  </div>
@@ -99,17 +151,17 @@
99
151
  </div>
100
152
  </div>
101
153
  <div
102
- v-if="hasOtherUserReplies(cmt)"
154
+ v-if="getHiddenCount(cmt) > 0"
103
155
  class="pl-[38px] flex flex-col gap-[16px]"
104
156
  >
105
157
  <Button
106
158
  variant="text"
107
- class="justify-start text-primary p-0 h-auto gap-[4px]"
159
+ class="justify-start text-primary h-auto gap-[4px] w-fit"
108
160
  @click="onToggleReplyComment(cmt.id)"
109
161
  >
110
162
  <div class="font-body-large-prominent">
111
163
  การตอบกลับ
112
- {{ $convert.convertNumber(getOtherUserRepliesCount(cmt)) }}
164
+ {{ $convert.convertNumber(getHiddenCount(cmt)) }}
113
165
  รายการ
114
166
  </div>
115
167
  <Icon
@@ -154,64 +206,119 @@
154
206
  </div>
155
207
  </div>
156
208
  <div
157
- v-if="openedComments.includes(cmt.id) || hasCurrentUserReplies(cmt)"
158
- 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]"
159
212
  >
160
213
  <div
161
214
  v-for="reply in cmt.reply"
162
- v-show="
163
- openedComments.includes(cmt.id) || reply.user.id === currentUser.id
164
- "
215
+ v-show="isReplyVisible(cmt, reply)"
165
216
  :key="reply.id"
166
- :data-reply-id="reply.id"
167
217
  class="flex flex-col gap-[16px]"
168
218
  >
169
- <div class="flex flex-col gap-[4px] w-full">
170
- <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
+ >
171
225
  <Avatar
172
226
  :size="30"
173
227
  :src="reply.user.avatar"
174
228
  class="cursor-pointer"
175
229
  @click="onViewProfileComment(reply.user.id)"
176
230
  />
177
- <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>
178
261
  <div
179
- class="font-body-large-prominent cursor-pointer"
180
- @click="onViewProfileComment(reply.user.id)"
262
+ class="flex flex-col bg-bright p-[8px] rounded-lg gap-[4px]"
181
263
  >
182
- {{ 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>
183
303
  </div>
184
304
  <div
185
- :ref="
186
- (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'
187
308
  "
188
- class="font-body-large whitespace-pre-wrap"
189
309
  >
190
- <template
191
- v-if="
192
- isContentExpanded(`reply-${reply.id}`) || !isContentOverflowing(`reply-${reply.id}`)
193
- "
194
- ><span
195
- v-if="reply.reply_to"
196
- @click="onViewProfileComment(reply.reply_to.id)"
197
- class="text-primary cursor-pointer mr-1"
198
- >{{ reply.reply_to.name }}</span
199
- >{{ normalizeContent(reply.content) }}</template
200
- >
201
- <template v-else
202
- ><span
203
- v-if="reply.reply_to"
204
- @click="onViewProfileComment(reply.reply_to.id)"
205
- class="text-primary cursor-pointer mr-1"
206
- >{{ reply.reply_to.name }}</span
207
- >{{ getTruncatedText(`reply-${reply.id}`) }}...<span
208
- class="text-primary cursor-pointer"
209
- @click="toggleContentExpand(`reply-${reply.id}`)"
210
- >ดูเพิ่มเติม</span
211
- ></template
212
- >
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
+ />
213
320
  </div>
214
- </div>
321
+ </template>
215
322
  </div>
216
323
  <div class="pl-[38px] flex gap-[16px] text-gray">
217
324
  <div class="font-body-medium">
@@ -227,7 +334,7 @@
227
334
  "
228
335
  :class="[reply.is_liked && 'text-primary']"
229
336
  />
230
- <div :class="[reply.is_liked && 'text-primary']">
337
+ <div v-if="reply.like_count > 0" :class="[reply.is_liked && 'text-primary']">
231
338
  {{ $convert.convertNumber(reply.like_count) }}
232
339
  </div>
233
340
  </div>
@@ -249,7 +356,7 @@
249
356
  <div
250
357
  :ref="(el) => setReplyInputRef(el, reply.id)"
251
358
  contenteditable="true"
252
- 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"
253
360
  data-placeholder="เขียนการตอบกลับ..."
254
361
  @input="onReplyInput($event, reply.id)"
255
362
  @keydown.enter.exact.prevent="onSendReplyComment(cmt)"
@@ -282,7 +389,7 @@
282
389
  </template>
283
390
 
284
391
  <script setup>
285
- import { ref, nextTick, computed } from "vue";
392
+ import { ref, nextTick, watch } from "vue";
286
393
  import { useRouter } from "vue-router";
287
394
  import { useConvert } from "@/runtime/composables/useConvert";
288
395
  const props = defineProps({
@@ -290,10 +397,9 @@ const props = defineProps({
290
397
  commentCount: { type: Number, required: true },
291
398
  currentUser: { type: Object, required: true }
292
399
  });
293
- 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"]);
294
401
  const router = useRouter();
295
402
  const { convertDateTorelativeText } = useConvert();
296
- const currentUser = computed(() => props.currentUser);
297
403
  const openedComments = ref([]);
298
404
  const expandedContentIds = ref([]);
299
405
  const truncatedTexts = ref({});
@@ -304,6 +410,48 @@ const replyingToId = ref(null);
304
410
  const replyToUser = ref(null);
305
411
  const replyInputRefs = ref({});
306
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
+ };
307
455
  const onToggleReplyComment = (id) => {
308
456
  if (openedComments.value.includes(id)) {
309
457
  openedComments.value = openedComments.value.filter((i) => i !== id);
@@ -398,21 +546,17 @@ const setReplyInputRef = (el, id) => {
398
546
  else delete replyInputRefs.value[id];
399
547
  };
400
548
  const onReplyComment = (id, user) => {
549
+ const isSelf = isSelfUser(user);
401
550
  replyingToId.value = id;
402
- replyToUser.value = user.id !== currentUser.value.id ? user : null;
551
+ replyToUser.value = isSelf ? null : user;
403
552
  replyTextMap.value[id] = "";
404
553
  nextTick(() => {
405
554
  const el = replyInputRefs.value[id];
406
555
  if (!el) return;
407
556
  el.innerHTML = "";
408
- if (user.id !== currentUser.value.id) {
409
- const chip = document.createElement("span");
410
- chip.dataset.type = "mention";
411
- chip.contentEditable = "false";
412
- chip.className = "inline-flex items-center text-primary font-body-large-prominent select-none mr-1";
413
- chip.textContent = user.name;
557
+ if (!isSelf) {
414
558
  const space = document.createTextNode("\xA0");
415
- el.appendChild(chip);
559
+ el.appendChild(buildMentionChip(user.name));
416
560
  el.appendChild(space);
417
561
  const range = document.createRange();
418
562
  const sel = window.getSelection();
@@ -500,7 +644,88 @@ const onToggleLikeReplyComment = (reply) => {
500
644
  const onViewProfileComment = (id) => {
501
645
  if (id) router.push(`/@${id}`);
502
646
  };
503
- const getOtherUserRepliesCount = (cmt) => (cmt.reply || []).filter((r) => r.user?.id !== currentUser.value?.id).length;
504
- const hasOtherUserReplies = (cmt) => getOtherUserRepliesCount(cmt) > 0;
505
- 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
+ ];
506
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;
@@ -64,15 +64,15 @@ declare const __VLS_export: import("vue").DefineComponent<ImageCropperProps, {
64
64
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<ImageCropperProps> & Readonly<{}>, {
65
65
  src: string;
66
66
  center: boolean;
67
+ background: boolean;
68
+ modal: boolean;
67
69
  responsive: boolean;
68
70
  restore: boolean;
69
71
  checkCrossOrigin: boolean;
70
72
  checkOrientation: boolean;
71
73
  crossorigin: "" | "anonymous" | "use-credentials";
72
- modal: boolean;
73
74
  guides: boolean;
74
75
  highlight: boolean;
75
- background: boolean;
76
76
  autoCrop: boolean;
77
77
  movable: boolean;
78
78
  rotatable: boolean;
@@ -64,15 +64,15 @@ declare const __VLS_export: import("vue").DefineComponent<ImageCropperProps, {
64
64
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<ImageCropperProps> & Readonly<{}>, {
65
65
  src: string;
66
66
  center: boolean;
67
+ background: boolean;
68
+ modal: boolean;
67
69
  responsive: boolean;
68
70
  restore: boolean;
69
71
  checkCrossOrigin: boolean;
70
72
  checkOrientation: boolean;
71
73
  crossorigin: "" | "anonymous" | "use-credentials";
72
- modal: boolean;
73
74
  guides: boolean;
74
75
  highlight: boolean;
75
- background: boolean;
76
76
  autoCrop: boolean;
77
77
  movable: boolean;
78
78
  rotatable: boolean;
@@ -31,8 +31,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
31
31
  fullHeight: boolean;
32
32
  name: string;
33
33
  limit: number;
34
- accept: string;
35
34
  disabledErrorMessage: boolean;
35
+ accept: string;
36
36
  labelIcon: string;
37
37
  disabledDrop: boolean;
38
38
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -31,8 +31,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
31
31
  fullHeight: boolean;
32
32
  name: string;
33
33
  limit: number;
34
- accept: string;
35
34
  disabledErrorMessage: boolean;
35
+ accept: string;
36
36
  labelIcon: string;
37
37
  disabledDrop: boolean;
38
38
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -22,8 +22,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
22
22
  }>, {
23
23
  id: string;
24
24
  name: string;
25
- disabledForgotPassword: boolean;
26
25
  new: boolean;
26
+ disabledForgotPassword: boolean;
27
27
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
28
  declare const _default: typeof __VLS_export;
29
29
  export default _default;
@@ -22,8 +22,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
22
22
  }>, {
23
23
  id: string;
24
24
  name: string;
25
- disabledForgotPassword: boolean;
26
25
  new: boolean;
26
+ disabledForgotPassword: boolean;
27
27
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
28
  declare const _default: typeof __VLS_export;
29
29
  export default _default;
@@ -12,9 +12,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
12
12
  label: string;
13
13
  color: InputSliderColor;
14
14
  fullWidth: boolean;
15
- step: number;
16
15
  max: number;
17
16
  min: number;
17
+ step: number;
18
18
  lineHeight: number | string;
19
19
  appearance: boolean;
20
20
  thumbSize: number | string;