pukaad-ui-lib 1.350.0 → 1.352.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
3
  "configKey": "pukaadUI",
4
- "version": "1.350.0",
4
+ "version": "1.352.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -26,8 +26,8 @@
26
26
  name="lucide:clock"
27
27
  :size="20"
28
28
  :class="[
29
- 'text-primary shrink-0 mt-[2px]',
30
- props.item.open_status === 'temporarily_closed' && 'text-destructive'
29
+ 'shrink-0 mt-[2px]',
30
+ props.item.open_status === 'temporarily_closed' ? 'text-destructive' : 'text-primary '
31
31
  ]"
32
32
  />
33
33
  <div v-if="businessHours.length" class="flex flex-col gap-[4px] flex-1">
@@ -1,10 +1,14 @@
1
- import type { CardReviewProps } from "#pukaad-ui/types/components/card/card-review";
1
+ import type { CardReviewProps, Replies } from "#pukaad-ui/types/components/card/card-review";
2
2
  type __VLS_Props = {
3
3
  item: CardReviewProps;
4
4
  isProfile: boolean;
5
5
  disabledLike?: boolean;
6
6
  disabledMenu?: boolean;
7
- /** ถ้ามี place context like/unlike และ edit จัดการใน component เลย */
7
+ /** เปิดให้ตอบกลับรีวิวได้ (สำหรับ business เจ้าของสถานที่เท่านั้น) เมนู "ตอบกลับ" จะโผล่ในปุ่ม */
8
+ allowReply?: boolean;
9
+ /** avatar ของ business ที่ใช้แสดงในฟอร์มตอบกลับ (Figma 2456:53296) */
10
+ replyAvatar?: string;
11
+ /** ถ้ามี place context → like/unlike, edit และ reply จัดการใน component เลย */
8
12
  place?: {
9
13
  id: string | number;
10
14
  name?: string;
@@ -18,17 +22,25 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
18
22
  "toggle-like": (item: CardReviewProps, liked: boolean) => any;
19
23
  "select-image": (item: CardReviewProps, index: number) => any;
20
24
  "review-updated": (data: unknown) => any;
25
+ "reply-updated": (item: CardReviewProps, reply?: Replies | undefined) => any;
26
+ "reply-submit": (item: CardReviewProps, description: string) => any;
27
+ "reply-delete": (item: CardReviewProps) => any;
21
28
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
22
29
  onDelete?: ((item: CardReviewProps) => any) | undefined;
23
30
  onEdit?: ((item: CardReviewProps) => any) | undefined;
24
31
  "onToggle-like"?: ((item: CardReviewProps, liked: boolean) => any) | undefined;
25
32
  "onSelect-image"?: ((item: CardReviewProps, index: number) => any) | undefined;
26
33
  "onReview-updated"?: ((data: unknown) => any) | undefined;
34
+ "onReply-updated"?: ((item: CardReviewProps, reply?: Replies | undefined) => any) | undefined;
35
+ "onReply-submit"?: ((item: CardReviewProps, description: string) => any) | undefined;
36
+ "onReply-delete"?: ((item: CardReviewProps) => any) | undefined;
27
37
  }>, {
28
38
  item: CardReviewProps;
29
39
  disabledMenu: boolean;
30
40
  isProfile: boolean;
31
41
  disabledLike: boolean;
42
+ allowReply: boolean;
43
+ replyAvatar: string;
32
44
  place: {
33
45
  id: string | number;
34
46
  name?: string;
@@ -44,6 +44,7 @@
44
44
  disabled-padding
45
45
  @review-edit="onEdit"
46
46
  @review-delete="emit('delete', props.item)"
47
+ @review-reply="openReplyForm"
47
48
  />
48
49
  </div>
49
50
  </div>
@@ -78,16 +79,46 @@
78
79
  </div>
79
80
  </div>
80
81
 
81
- <div v-if="props.item.replies">
82
- <div class="p-[16px] rounded-sm bg-bright">
82
+ <div
83
+ v-if="localReplies && !isReplyOpen"
84
+ class="relative p-[16px] rounded-md bg-bright flex flex-col gap-[4px]"
85
+ >
86
+ <!-- หางบอลลูนชี้ขึ้นบน (Figma 2456:53435 Polygon 8) -->
87
+ <div
88
+ class="absolute -top-[9px] left-[8px] w-0 h-0 border-l-[9px] border-l-transparent border-r-[9px] border-r-transparent border-b-[12px] border-b-bright"
89
+ />
90
+ <div class="flex justify-between items-center">
83
91
  <div class="text-gray font-body-large">
84
- การตอบกลับจาก {{ props.item.replies?.author }}
85
- </div>
86
- <div class="font-body-large">
87
- {{ props.item.replies?.description }}
92
+ การตอบกลับจาก {{ localReplies.author }}
88
93
  </div>
94
+ <!-- เมนูจัดการคำตอบกลับของตัวเอง — เฉพาะ business (Figma 2456:88751) -->
95
+ <PickerOptionMenuUser
96
+ v-if="props.allowReply"
97
+ state="my-reply"
98
+ disabled-padding
99
+ @reply-edit="openReplyForm"
100
+ @reply-delete="emit('reply-delete', props.item)"
101
+ />
102
+ </div>
103
+ <!-- whitespace-pre-wrap ให้ \n จาก Shift+Enter แสดงจริง (เทียบ comment.vue) -->
104
+ <div class="font-body-large whitespace-pre-wrap break-words">
105
+ {{ normalizeContent(localReplies.description) }}
89
106
  </div>
90
107
  </div>
108
+
109
+ <!-- ฟอร์มตอบกลับ (business เท่านั้น) — Figma 2456:53296/53525
110
+ ไม่ส่ง reply-to → ชื่อคนอยู่ใน placeholder เฉยๆ ไม่ปนลงไปในข้อความที่ส่ง -->
111
+ <InputComment
112
+ v-if="isReplyOpen"
113
+ v-model="replyText"
114
+ :avatar="props.replyAvatar"
115
+ :placeholder="`\u0E15\u0E2D\u0E1A\u0E01\u0E25\u0E31\u0E1A ${props.item.user?.name ?? ''}`"
116
+ :loading="isSubmittingReply"
117
+ submit-text="ตอบกลับ"
118
+ autofocus
119
+ @submit="submitReply"
120
+ @cancel="isReplyOpen = false"
121
+ />
91
122
  </div>
92
123
  </div>
93
124
 
@@ -148,9 +179,11 @@ const props = defineProps({
148
179
  isProfile: { type: Boolean, required: true, default: false },
149
180
  disabledLike: { type: Boolean, required: false, default: false },
150
181
  disabledMenu: { type: Boolean, required: false, default: false },
182
+ allowReply: { type: Boolean, required: false, default: false },
183
+ replyAvatar: { type: String, required: false, default: "" },
151
184
  place: { type: Object, required: false, default: void 0 }
152
185
  });
153
- const emit = defineEmits(["toggle-like", "select-image", "edit", "delete", "review-updated"]);
186
+ const emit = defineEmits(["toggle-like", "select-image", "edit", "delete", "review-updated", "reply-updated", "reply-submit", "reply-delete"]);
154
187
  const isOpen = ref(false);
155
188
  const startIndex = ref(0);
156
189
  const liked = ref(props.item.review?.liked ?? false);
@@ -178,9 +211,11 @@ const drawerItem = computed(() => ({
178
211
  description: props.item.review?.description ?? "",
179
212
  photos: (props.item.review?.images ?? []).map((url) => ({ url }))
180
213
  }));
181
- const menuType = computed(
182
- () => props.item.user?.is_my_review ? "my-review" : "report-review"
183
- );
214
+ const menuType = computed(() => {
215
+ if (props.item.user?.is_my_review) return "my-review";
216
+ if (props.allowReply && !localReplies.value) return "business-review";
217
+ return "report-review";
218
+ });
184
219
  const navigateToProfile = (pathName) => {
185
220
  if (!pathName) return;
186
221
  router.push(`/@${pathName}`);
@@ -221,6 +256,56 @@ const onEdit = () => {
221
256
  const onReviewSuccess = (data) => {
222
257
  emit("review-updated", data);
223
258
  };
259
+ const localReplies = ref(props.item.replies);
260
+ const isReplyOpen = ref(false);
261
+ const replyText = ref("");
262
+ const isSubmittingReply = ref(false);
263
+ const normalizeContent = (text) => text.replace(/\n{3,}/g, "\n\n");
264
+ watch(
265
+ () => props.item.replies,
266
+ (val) => {
267
+ localReplies.value = val;
268
+ }
269
+ );
270
+ const openReplyForm = () => {
271
+ replyText.value = localReplies.value?.description ?? "";
272
+ isReplyOpen.value = true;
273
+ };
274
+ const submitReply = async () => {
275
+ const description = replyText.value.trim();
276
+ if (!description || isSubmittingReply.value) return;
277
+ if (!props.place?.id) {
278
+ emit("reply-submit", props.item, description);
279
+ isReplyOpen.value = false;
280
+ return;
281
+ }
282
+ isSubmittingReply.value = true;
283
+ try {
284
+ const isEdit = !!localReplies.value;
285
+ const res = await api(
286
+ `/suggest-places/${props.place.id}/reviews/${props.item.review_id}/reply`,
287
+ {
288
+ method: isEdit ? "patch" : "post",
289
+ body: { description }
290
+ }
291
+ );
292
+ localReplies.value = res?.data ?? {
293
+ replies_id: localReplies.value?.replies_id ?? 0,
294
+ author: localReplies.value?.author ?? "",
295
+ description,
296
+ created_at: localReplies.value?.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
297
+ };
298
+ emit("reply-updated", props.item, localReplies.value);
299
+ isReplyOpen.value = false;
300
+ $toast.success(
301
+ isEdit ? "\u0E41\u0E01\u0E49\u0E44\u0E02\u0E01\u0E32\u0E23\u0E15\u0E2D\u0E1A\u0E01\u0E25\u0E31\u0E1A\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08 !" : "\u0E15\u0E2D\u0E1A\u0E01\u0E25\u0E31\u0E1A\u0E23\u0E35\u0E27\u0E34\u0E27\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08 !"
302
+ );
303
+ } catch {
304
+ $toast.error("\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E25\u0E2D\u0E07\u0E43\u0E2B\u0E21\u0E48\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07");
305
+ } finally {
306
+ isSubmittingReply.value = false;
307
+ }
308
+ };
224
309
  const selectImage = (index) => {
225
310
  startIndex.value = index;
226
311
  isOpen.value = true;
@@ -1,10 +1,14 @@
1
- import type { CardReviewProps } from "#pukaad-ui/types/components/card/card-review";
1
+ import type { CardReviewProps, Replies } from "#pukaad-ui/types/components/card/card-review";
2
2
  type __VLS_Props = {
3
3
  item: CardReviewProps;
4
4
  isProfile: boolean;
5
5
  disabledLike?: boolean;
6
6
  disabledMenu?: boolean;
7
- /** ถ้ามี place context like/unlike และ edit จัดการใน component เลย */
7
+ /** เปิดให้ตอบกลับรีวิวได้ (สำหรับ business เจ้าของสถานที่เท่านั้น) เมนู "ตอบกลับ" จะโผล่ในปุ่ม */
8
+ allowReply?: boolean;
9
+ /** avatar ของ business ที่ใช้แสดงในฟอร์มตอบกลับ (Figma 2456:53296) */
10
+ replyAvatar?: string;
11
+ /** ถ้ามี place context → like/unlike, edit และ reply จัดการใน component เลย */
8
12
  place?: {
9
13
  id: string | number;
10
14
  name?: string;
@@ -18,17 +22,25 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
18
22
  "toggle-like": (item: CardReviewProps, liked: boolean) => any;
19
23
  "select-image": (item: CardReviewProps, index: number) => any;
20
24
  "review-updated": (data: unknown) => any;
25
+ "reply-updated": (item: CardReviewProps, reply?: Replies | undefined) => any;
26
+ "reply-submit": (item: CardReviewProps, description: string) => any;
27
+ "reply-delete": (item: CardReviewProps) => any;
21
28
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
22
29
  onDelete?: ((item: CardReviewProps) => any) | undefined;
23
30
  onEdit?: ((item: CardReviewProps) => any) | undefined;
24
31
  "onToggle-like"?: ((item: CardReviewProps, liked: boolean) => any) | undefined;
25
32
  "onSelect-image"?: ((item: CardReviewProps, index: number) => any) | undefined;
26
33
  "onReview-updated"?: ((data: unknown) => any) | undefined;
34
+ "onReply-updated"?: ((item: CardReviewProps, reply?: Replies | undefined) => any) | undefined;
35
+ "onReply-submit"?: ((item: CardReviewProps, description: string) => any) | undefined;
36
+ "onReply-delete"?: ((item: CardReviewProps) => any) | undefined;
27
37
  }>, {
28
38
  item: CardReviewProps;
29
39
  disabledMenu: boolean;
30
40
  isProfile: boolean;
31
41
  disabledLike: boolean;
42
+ allowReply: boolean;
43
+ replyAvatar: string;
32
44
  place: {
33
45
  id: string | number;
34
46
  name?: string;
@@ -7,43 +7,17 @@
7
7
  </div>
8
8
  </div>
9
9
 
10
- <div class="flex flex-col gap-[16px]">
11
- <div class="flex gap-[8px] w-full">
12
- <Avatar :size="30" :src="currentUser.avatar" />
13
- <div ref="mainInputRef" contenteditable="true"
14
- 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"
15
- data-placeholder="เพิ่มความคิดเห็น" @focus="isShowMainActions = true" @input="onMainCommentInput"
16
- @keydown.enter.exact.prevent="onSendComment" @keydown.shift.enter.prevent="onInsertLineBreak" />
17
- </div>
18
- <div v-if="isShowMainActions" class="flex justify-end gap-[8px]">
19
- <Button variant="outline" class="w-[56px]" @click="onCancelComment">
20
- ยกเลิก
21
- </Button>
22
- <Button color="primary" class="w-[56px]" :disabled="!mainCommentInput" @click="onSendComment">
23
- ส่ง
24
- </Button>
25
- </div>
26
- </div>
10
+ <!-- กล่องคอมเมนต์หลัก — ปุ่มโผล่ตอนคลิกเข้าช่องพิมพ์ (actions-on-focus) -->
11
+ <InputComment ref="mainInputRef" v-model="mainCommentInput" :avatar="currentUser.avatar"
12
+ placeholder="เพิ่มความคิดเห็น" actions-on-focus @submit="onSendComment" />
27
13
  <div v-for="cmt in resolvedComments" :key="cmt.id" class="flex flex-col gap-[16px] w-full">
28
14
  <div class="flex flex-col gap-[4px] w-full">
29
15
  <div class="flex gap-[8px]" @mouseenter="hoveredId = cmt.id" @mouseleave="hoveredId = null">
30
16
  <Avatar :size="30" :src="cmt.user.avatar" class="cursor-pointer" @click="onViewProfileComment(cmt.user.id)" />
31
17
  <template v-if="editingId === cmt.id">
32
- <div class="flex-1 flex flex-col gap-[4px]">
33
- <div :ref="(el) => setEditInputRef(el, cmt.id)" contenteditable="true"
34
- 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"
35
- @input="onEditInput($event, cmt.id)" @keydown.enter.exact.prevent="onSendEdit(cmt, 'comment')"
36
- @keydown.shift.enter.prevent="onInsertLineBreak" />
37
- <div class="flex justify-end gap-[8px]">
38
- <Button variant="outline" class="w-[56px]" @click="onCancelEdit">
39
- ยกเลิก
40
- </Button>
41
- <Button color="primary" class="w-[56px]" :disabled="!editTextMap[cmt.id]"
42
- @click="onSendEdit(cmt, 'comment')">
43
- ส่ง
44
- </Button>
45
- </div>
46
- </div>
18
+ <!-- กล่องแก้ไข อวาตาร์ใช้ของแถวข้างนอก เลย disabled-avatar -->
19
+ <InputComment v-model="editText" disabled-avatar autofocus class="flex-1 min-w-0"
20
+ @submit="onSendEdit(cmt, 'comment')" @cancel="onCancelEdit" />
47
21
  </template>
48
22
  <template v-else>
49
23
  <div class="flex flex-col gap-[4px] bg-bright p-[8px] rounded-lg">
@@ -105,25 +79,10 @@
105
79
  </Button>
106
80
  </div>
107
81
  </div>
108
- <div v-if="replyingToId === cmt.id" class="flex flex-col gap-[16px]">
109
- <div class="flex gap-[8px] w-full">
110
- <Avatar :size="30" :src="currentUser.avatar" />
111
- <div class="flex-1">
112
- <div :ref="(el) => setReplyInputRef(el, cmt.id)" contenteditable="true"
113
- 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"
114
- data-placeholder="เขียนการตอบกลับ..." @input="onReplyInput($event, cmt.id)"
115
- @keydown.enter.exact.prevent="onSendReplyComment(cmt)" @keydown.shift.enter.prevent="onInsertLineBreak" />
116
- </div>
117
- </div>
118
- <div class="flex justify-end gap-[8px]">
119
- <Button variant="outline" class="w-[56px]" @click="onCancelReplyComment">
120
- ยกเลิก
121
- </Button>
122
- <Button color="primary" class="w-[68px]" :disabled="!replyTextMap[cmt.id]" @click="onSendReplyComment(cmt)">
123
- ตอบกลับ
124
- </Button>
125
- </div>
126
- </div>
82
+ <!-- กล่องตอบกลับคอมเมนต์ ชิป mention ผ่าน v-model:reply-to -->
83
+ <InputComment v-if="replyingToId === cmt.id" v-model="replyText" v-model:reply-to="replyToUser"
84
+ :avatar="currentUser.avatar" placeholder="เขียนการตอบกลับ..." submit-text="ตอบกลับ" autofocus
85
+ @submit="onSendReplyComment(cmt)" @cancel="onCancelReplyComment" />
127
86
  <div v-if="cmt.reply.length > 0" v-show="
128
87
  openedComments.includes(cmt.id) || cmt.reply.some((r) => pinnedReplyIds.has(r.id))
129
88
  " class="pl-[38px] flex flex-col gap-[16px]">
@@ -134,21 +93,9 @@
134
93
  <Avatar :size="30" :src="reply.user.avatar" class="cursor-pointer"
135
94
  @click="onViewProfileComment(reply.user.id)" />
136
95
  <template v-if="editingId === reply.id">
137
- <div class="flex-1 flex flex-col gap-[4px]">
138
- <div :ref="(el) => setEditInputRef(el, reply.id)" contenteditable="true"
139
- 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"
140
- @input="onEditInput($event, reply.id)" @keydown.enter.exact.prevent="onSendEdit(reply, 'reply')"
141
- @keydown.shift.enter.prevent="onInsertLineBreak" />
142
- <div class="flex justify-end gap-[8px]">
143
- <Button variant="outline" class="w-[56px]" @click="onCancelEdit">
144
- ยกเลิก
145
- </Button>
146
- <Button color="primary" class="w-[56px]" :disabled="!editTextMap[reply.id]"
147
- @click="onSendEdit(reply, 'reply')">
148
- ส่ง
149
- </Button>
150
- </div>
151
- </div>
96
+ <!-- แก้ไข reply — ชิป reply_to โชว์ผ่าน v-model:reply-to (ข้อความที่ส่งไม่รวมชื่อ) -->
97
+ <InputComment v-model="editText" v-model:reply-to="editReplyTo" disabled-avatar autofocus
98
+ class="flex-1 min-w-0" @submit="onSendEdit(reply, 'reply')" @cancel="onCancelEdit" />
152
99
  </template>
153
100
  <template v-else>
154
101
  <div class="flex flex-col bg-bright p-[8px] rounded-lg gap-[4px]">
@@ -199,27 +146,10 @@
199
146
  </div>
200
147
  </div>
201
148
  </div>
202
- <div v-if="replyingToId === reply.id" class="flex flex-col gap-[16px]">
203
- <div class="flex gap-[8px] w-full">
204
- <Avatar :size="30" :src="currentUser.avatar" />
205
- <div class="flex-1">
206
- <div :ref="(el) => setReplyInputRef(el, reply.id)" contenteditable="true"
207
- 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"
208
- data-placeholder="เขียนการตอบกลับ..." @input="onReplyInput($event, reply.id)"
209
- @keydown.enter.exact.prevent="onSendReplyComment(cmt)"
210
- @keydown.shift.enter.prevent="onInsertLineBreak" />
211
- </div>
212
- </div>
213
- <div class="flex justify-end gap-[8px]">
214
- <Button variant="outline" class="w-[56px]" @click="onCancelReplyComment">
215
- ยกเลิก
216
- </Button>
217
- <Button color="primary" class="w-[68px]" :disabled="!replyTextMap[reply.id]"
218
- @click="onSendReplyComment(cmt)">
219
- ตอบกลับ
220
- </Button>
221
- </div>
222
- </div>
149
+ <!-- ตอบกลับ reply submit ยังผูกกับ comment แม่ (cmt) เหมือนเดิม -->
150
+ <InputComment v-if="replyingToId === reply.id" v-model="replyText" v-model:reply-to="replyToUser"
151
+ :avatar="currentUser.avatar" placeholder="เขียนการตอบกลับ..." submit-text="ตอบกลับ" autofocus
152
+ @submit="onSendReplyComment(cmt)" @cancel="onCancelReplyComment" />
223
153
  </div>
224
154
  </div>
225
155
  </div>
@@ -270,11 +200,9 @@ const expandedContentIds = ref([]);
270
200
  const truncatedTexts = ref({});
271
201
  const mainInputRef = ref(null);
272
202
  const mainCommentInput = ref("");
273
- const isShowMainActions = ref(false);
274
203
  const replyingToId = ref(null);
275
- const replyToUser = ref(null);
276
- const replyInputRefs = ref({});
277
- const replyTextMap = ref({});
204
+ const replyToUser = ref(void 0);
205
+ const replyText = ref("");
278
206
  const pinnedReplyIds = ref(/* @__PURE__ */ new Set());
279
207
  const knownRepliesPerComment = /* @__PURE__ */ new WeakMap();
280
208
  watch(
@@ -309,17 +237,9 @@ const hoveredId = ref(null);
309
237
  const openMenuId = ref(null);
310
238
  const isMenuVisible = (id) => hoveredId.value === id || openMenuId.value === id;
311
239
  const editingId = ref(null);
312
- const editInputRefs = ref({});
313
- const editTextMap = ref({});
240
+ const editText = ref("");
241
+ const editReplyTo = ref(void 0);
314
242
  const isSelfUser = (user) => !!props.currentUser.id && user.id === props.currentUser.id;
315
- const buildMentionChip = (name) => {
316
- const chip = document.createElement("span");
317
- chip.dataset.type = "mention";
318
- chip.contentEditable = "false";
319
- chip.className = "inline-flex items-center text-primary font-body-large-prominent select-none mr-1";
320
- chip.textContent = name;
321
- return chip;
322
- };
323
243
  const onToggleReplyComment = (id) => {
324
244
  if (openedComments.value.includes(id)) {
325
245
  openedComments.value = openedComments.value.filter((i) => i !== id);
@@ -395,113 +315,26 @@ const toggleContentExpand = (id) => {
395
315
  expandedContentIds.value.push(id);
396
316
  }
397
317
  };
398
- const clearMainInput = () => {
399
- mainCommentInput.value = "";
400
- if (mainInputRef.value) mainInputRef.value.innerText = "";
401
- };
402
- const onCancelComment = () => {
403
- clearMainInput();
404
- isShowMainActions.value = false;
405
- };
406
318
  const onSendComment = () => {
407
319
  if (!mainCommentInput.value.trim()) return;
408
320
  emit("submit", mainCommentInput.value.trim());
409
- clearMainInput();
410
- isShowMainActions.value = false;
411
- };
412
- const setReplyInputRef = (el, id) => {
413
- if (el) replyInputRefs.value[id] = el;
414
- else delete replyInputRefs.value[id];
321
+ mainInputRef.value?.reset();
415
322
  };
416
323
  const onReplyComment = (id, user) => {
417
- const isSelf = isSelfUser(user);
418
324
  replyingToId.value = id;
419
- replyToUser.value = isSelf ? null : user;
420
- replyTextMap.value[id] = "";
421
- nextTick(() => {
422
- const el = replyInputRefs.value[id];
423
- if (!el) return;
424
- el.innerHTML = "";
425
- if (!isSelf) {
426
- const space = document.createTextNode("\xA0");
427
- el.appendChild(buildMentionChip(user.name));
428
- el.appendChild(space);
429
- const range = document.createRange();
430
- const sel = window.getSelection();
431
- range.setStartAfter(space);
432
- range.collapse(true);
433
- sel?.removeAllRanges();
434
- sel?.addRange(range);
435
- }
436
- el.focus();
437
- });
438
- };
439
- const readTextWithBreaks = (el, skipMention = false) => {
440
- const nodes = Array.from(el.childNodes);
441
- while (nodes.length && nodes.at(-1)?.nodeName === "BR") nodes.pop();
442
- let text = "";
443
- nodes.forEach((node) => {
444
- if (skipMention && node.dataset?.type === "mention")
445
- return;
446
- if (node.nodeName === "BR") {
447
- text += "\n";
448
- return;
449
- }
450
- if (node.nodeName === "DIV") {
451
- text += "\n" + node.innerText;
452
- return;
453
- }
454
- text += node.textContent ?? "";
455
- });
456
- return text.replace(/\n{3,}/g, "\n\n").replace(/^\u00A0/, "").trim();
457
- };
458
- const onInsertLineBreak = (event) => {
459
- const el = event.target;
460
- const sel = window.getSelection();
461
- if (!sel || sel.rangeCount === 0) return;
462
- const range = sel.getRangeAt(0);
463
- range.deleteContents();
464
- const br = document.createElement("br");
465
- range.insertNode(br);
466
- if (!br.nextSibling || br.nextSibling.nodeName !== "BR" && br.nextSibling.textContent === "") {
467
- const phantom = document.createElement("br");
468
- br.after(phantom);
469
- }
470
- range.setStartAfter(br);
471
- range.collapse(true);
472
- sel.removeAllRanges();
473
- sel.addRange(range);
474
- el.dispatchEvent(new Event("input", { bubbles: true }));
475
- };
476
- const onMainCommentInput = (event) => {
477
- const el = event.target;
478
- mainCommentInput.value = readTextWithBreaks(el);
479
- };
480
- const onReplyInput = (event, id) => {
481
- const el = event.target;
482
- const chipExists = !!el.querySelector('[data-type="mention"]');
483
- if (!chipExists) replyToUser.value = null;
484
- replyTextMap.value[id] = readTextWithBreaks(el, true);
325
+ replyToUser.value = isSelfUser(user) ? void 0 : user;
326
+ replyText.value = "";
485
327
  };
486
328
  const onCancelReplyComment = () => {
487
- const id = replyingToId.value;
488
- if (id && replyInputRefs.value[id]) {
489
- replyInputRefs.value[id].innerHTML = "";
490
- delete replyTextMap.value[id];
491
- }
492
329
  replyingToId.value = null;
493
- replyToUser.value = null;
330
+ replyToUser.value = void 0;
331
+ replyText.value = "";
494
332
  };
495
333
  const onSendReplyComment = (comment) => {
496
- const id = replyingToId.value;
497
- if (!id) return;
498
- const content = replyTextMap.value[id]?.trim() ?? "";
334
+ const content = replyText.value.trim();
499
335
  if (!content) return;
500
336
  emit("reply", comment.id, replyToUser.value?.id, content);
501
- if (replyInputRefs.value[id]) replyInputRefs.value[id].innerHTML = "";
502
- delete replyTextMap.value[id];
503
- replyingToId.value = null;
504
- replyToUser.value = null;
337
+ onCancelReplyComment();
505
338
  };
506
339
  const onToggleLikeComment = (comment) => {
507
340
  emit("like-comment", comment);
@@ -512,47 +345,22 @@ const onToggleLikeReplyComment = (reply) => {
512
345
  const onViewProfileComment = (id) => {
513
346
  if (id) router.push(`/@${id}`);
514
347
  };
515
- const setEditInputRef = (el, id) => {
516
- if (el) editInputRefs.value[id] = el;
517
- else delete editInputRefs.value[id];
518
- };
519
348
  const onEditClick = (id, content, replyTo) => {
520
349
  editingId.value = id;
521
- editTextMap.value[id] = content;
522
- nextTick(() => {
523
- const el = editInputRefs.value[id];
524
- if (!el) return;
525
- el.innerHTML = "";
526
- if (replyTo) {
527
- el.appendChild(buildMentionChip(replyTo.name));
528
- el.appendChild(document.createTextNode("\xA0"));
529
- }
530
- el.appendChild(document.createTextNode(content));
531
- const range = document.createRange();
532
- range.selectNodeContents(el);
533
- range.collapse(false);
534
- const sel = window.getSelection();
535
- sel?.removeAllRanges();
536
- sel?.addRange(range);
537
- el.focus();
538
- });
539
- };
540
- const onEditInput = (event, id) => {
541
- const el = event.target;
542
- editTextMap.value[id] = readTextWithBreaks(el, true);
350
+ editText.value = content;
351
+ editReplyTo.value = replyTo;
543
352
  };
544
353
  const onCancelEdit = () => {
545
- const id = editingId.value;
546
- if (id) delete editTextMap.value[id];
547
354
  editingId.value = null;
355
+ editText.value = "";
356
+ editReplyTo.value = void 0;
548
357
  };
549
358
  const onSendEdit = (item, type) => {
550
- const content = editTextMap.value[item.id]?.trim() ?? "";
359
+ const content = editText.value.trim();
551
360
  if (!content) return;
552
361
  if (type === "comment") emit("edit-comment", item, content);
553
362
  else emit("edit-reply", item, content);
554
- delete editTextMap.value[item.id];
555
- editingId.value = null;
363
+ onCancelEdit();
556
364
  };
557
365
  const getCommentMenuItems = (comment) => isSelfUser(comment.user) ? [
558
366
  {