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 +1 -1
- package/dist/runtime/components/card/card-place-detail.vue +2 -2
- package/dist/runtime/components/card/card-review.d.vue.ts +14 -2
- package/dist/runtime/components/card/card-review.vue +95 -10
- package/dist/runtime/components/card/card-review.vue.d.ts +14 -2
- package/dist/runtime/components/comment.vue +34 -226
- package/dist/runtime/components/input/input-comment.d.vue.ts +51 -6
- package/dist/runtime/components/input/input-comment.vue +185 -31
- package/dist/runtime/components/input/input-comment.vue.d.ts +51 -6
- package/dist/runtime/components/label/label-card-content.d.vue.ts +24 -0
- package/dist/runtime/components/label/label-card-content.vue +105 -0
- package/dist/runtime/components/label/label-card-content.vue.d.ts +24 -0
- package/dist/runtime/components/picker/picker-option-menu/picker-option-menu-user.d.vue.ts +6 -0
- package/dist/runtime/components/picker/picker-option-menu/picker-option-menu-user.vue +38 -1
- package/dist/runtime/components/picker/picker-option-menu/picker-option-menu-user.vue.d.ts +6 -0
- package/dist/runtime/components/ui/popover/PopoverContent.d.vue.ts +1 -1
- package/dist/runtime/components/ui/popover/PopoverContent.vue.d.ts +1 -1
- package/package.json +1 -1
- package/dist/runtime/components/label/label-card-atom.d.vue.ts +0 -3
- package/dist/runtime/components/label/label-card-atom.vue +0 -17
- package/dist/runtime/components/label/label-card-atom.vue.d.ts +0 -3
package/dist/module.json
CHANGED
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
name="lucide:clock"
|
|
27
27
|
:size="20"
|
|
28
28
|
:class="[
|
|
29
|
-
'
|
|
30
|
-
props.item.open_status === 'temporarily_closed'
|
|
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
|
-
/**
|
|
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
|
|
82
|
-
|
|
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
|
-
การตอบกลับจาก {{
|
|
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
|
-
(
|
|
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
|
-
/**
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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(
|
|
276
|
-
const
|
|
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
|
|
313
|
-
const
|
|
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
|
-
|
|
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 =
|
|
420
|
-
|
|
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 =
|
|
330
|
+
replyToUser.value = void 0;
|
|
331
|
+
replyText.value = "";
|
|
494
332
|
};
|
|
495
333
|
const onSendReplyComment = (comment) => {
|
|
496
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
522
|
-
|
|
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 =
|
|
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
|
-
|
|
555
|
-
editingId.value = null;
|
|
363
|
+
onCancelEdit();
|
|
556
364
|
};
|
|
557
365
|
const getCommentMenuItems = (comment) => isSelfUser(comment.user) ? [
|
|
558
366
|
{
|