pukaad-ui-lib 1.351.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-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
|
@@ -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
|
{
|
|
@@ -1,13 +1,58 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HTMLAttributes } from "vue";
|
|
2
|
+
import type { User } from "#pukaad-ui/types/components/comment.d.ts";
|
|
3
|
+
export interface InputCommentProps {
|
|
4
|
+
/** อวาตาร์ของคนที่กำลังพิมพ์ — ส่ง undefined ได้ Avatar จะขึ้นตัวแทน */
|
|
5
|
+
avatar?: string;
|
|
6
|
+
/** ไม่ต้องวาดอวาตาร์ — ใช้ตอนที่แถวข้างนอกมีอวาตาร์อยู่แล้ว (กล่องแก้ไขของ Comment) */
|
|
7
|
+
disabledAvatar?: boolean;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** กำลังส่ง — ล็อกช่องพิมพ์ + spinner ที่ปุ่มส่ง */
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
submitText?: string;
|
|
13
|
+
cancelText?: string;
|
|
14
|
+
/** ซ่อนปุ่มไว้ก่อน โผล่ตอนคลิกเข้าช่องพิมพ์ (กล่องคอมเมนต์หลักของ Comment) */
|
|
15
|
+
actionsOnFocus?: boolean;
|
|
16
|
+
/** โฟกัส + วาง caret ท้ายข้อความทันทีที่ mount — สำหรับกล่องที่โผล่จากการกดปุ่ม (ตอบกลับ/แก้ไข) */
|
|
17
|
+
autofocus?: boolean;
|
|
18
|
+
/** ไม่ต้องมีปุ่ม — ให้ parent คุมเอง */
|
|
19
|
+
disabledActions?: boolean;
|
|
20
|
+
class?: HTMLAttributes["class"];
|
|
21
|
+
}
|
|
2
22
|
type __VLS_Props = InputCommentProps;
|
|
3
23
|
type __VLS_ModelProps = {
|
|
4
|
-
|
|
24
|
+
/** ข้อความล้วน — ไม่รวมชื่อคนที่ตอบกลับ (ชิป mention ถูกข้ามตอนอ่าน) */
|
|
25
|
+
modelValue?: string;
|
|
26
|
+
/**
|
|
27
|
+
* คนที่ถูกตอบกลับ → แสดงเป็นชิปสี primary หน้าข้อความ
|
|
28
|
+
* เป็น v-model เพราะผู้ใช้ลบชิปทิ้งได้ แล้วคอมโพเนนต์ต้องบอก parent กลับ
|
|
29
|
+
*/
|
|
30
|
+
"replyTo"?: User | undefined;
|
|
5
31
|
};
|
|
6
32
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
7
|
-
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
8
|
-
|
|
33
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
34
|
+
focus: () => void;
|
|
35
|
+
reset: () => void;
|
|
36
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
37
|
+
submit: (value: string) => any;
|
|
38
|
+
cancel: () => any;
|
|
39
|
+
"update:modelValue": (value: string) => any;
|
|
40
|
+
"update:replyTo": (value: User | undefined) => any;
|
|
9
41
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
10
|
-
|
|
11
|
-
|
|
42
|
+
onSubmit?: ((value: string) => any) | undefined;
|
|
43
|
+
onCancel?: (() => any) | undefined;
|
|
44
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
45
|
+
"onUpdate:replyTo"?: ((value: User | undefined) => any) | undefined;
|
|
46
|
+
}>, {
|
|
47
|
+
loading: boolean;
|
|
48
|
+
disabled: boolean;
|
|
49
|
+
placeholder: string;
|
|
50
|
+
disabledAvatar: boolean;
|
|
51
|
+
submitText: string;
|
|
52
|
+
cancelText: string;
|
|
53
|
+
actionsOnFocus: boolean;
|
|
54
|
+
autofocus: boolean;
|
|
55
|
+
disabledActions: boolean;
|
|
56
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
12
57
|
declare const _default: typeof __VLS_export;
|
|
13
58
|
export default _default;
|
|
@@ -1,43 +1,197 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
<!-- Figma "Input - Comments / Reply" (2456:53525 → 1981:2017): avatar + ช่องพิมพ์ / ปุ่มชิดขวา -->
|
|
3
|
+
<div :class="['flex flex-col gap-[16px] items-end w-full', props.class]">
|
|
4
|
+
<!-- focusin (ไม่ใช่ focus) เพราะต้องการให้ bubble ขึ้นมาจากช่องพิมพ์ข้างใน -->
|
|
5
|
+
<div class="flex gap-[8px] items-start w-full" @focusin="isFocused = true">
|
|
6
|
+
<!-- pt-[5px] ดันอวาตาร์ลงให้ตรงบรรทัดแรกของข้อความ (Figma 2477:33564) -->
|
|
7
|
+
<div
|
|
8
|
+
v-if="!props.disabledAvatar"
|
|
9
|
+
class="flex items-center pt-[5px] shrink-0"
|
|
10
|
+
>
|
|
11
|
+
<Avatar :src="props.avatar" :size="30" />
|
|
12
|
+
</div>
|
|
13
|
+
<div
|
|
14
|
+
ref="editorRef"
|
|
15
|
+
:contenteditable="isLocked ? 'false' : 'true'"
|
|
16
|
+
:data-placeholder="props.placeholder"
|
|
17
|
+
:class="[
|
|
18
|
+
'relative flex-1 min-w-0 min-h-[36px] px-[12px] py-[5px] rounded-md border border-mercury bg-white shadow-xs font-body-large text-black',
|
|
19
|
+
'whitespace-pre-wrap break-words outline-none focus:outline-none focus:ring-2 focus:ring-primary',
|
|
20
|
+
isEmpty && 'before:content-[attr(data-placeholder)] before:text-gray before:absolute before:pointer-events-none',
|
|
21
|
+
isLocked && 'opacity-50 cursor-not-allowed'
|
|
22
|
+
]"
|
|
23
|
+
@input="onInput"
|
|
24
|
+
@keydown.enter.exact.prevent="onEnter"
|
|
25
|
+
@keydown.shift.enter.prevent="onInsertLineBreak"
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div
|
|
30
|
+
v-if="isActionsVisible"
|
|
31
|
+
class="flex gap-[8px] items-center justify-end w-full"
|
|
32
|
+
>
|
|
33
|
+
<Button
|
|
34
|
+
variant="outline"
|
|
35
|
+
size="sm"
|
|
36
|
+
:disabled="props.loading"
|
|
37
|
+
@click="onCancel"
|
|
38
|
+
>
|
|
39
|
+
{{ props.cancelText }}
|
|
40
|
+
</Button>
|
|
41
|
+
<Button
|
|
42
|
+
color="primary"
|
|
43
|
+
size="sm"
|
|
44
|
+
:disabled="!modelValue.trim() || props.loading"
|
|
45
|
+
:loading="props.loading"
|
|
46
|
+
@click="onEnter"
|
|
47
|
+
>
|
|
48
|
+
{{ props.submitText }}
|
|
49
|
+
</Button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
8
52
|
</template>
|
|
9
53
|
|
|
10
54
|
<script setup>
|
|
11
|
-
import { ref, watch, onMounted, nextTick } from "vue";
|
|
55
|
+
import { ref, computed, watch, onMounted, nextTick } from "vue";
|
|
12
56
|
const props = defineProps({
|
|
13
|
-
|
|
57
|
+
avatar: { type: String, required: false },
|
|
58
|
+
disabledAvatar: { type: Boolean, required: false, default: false },
|
|
59
|
+
placeholder: { type: String, required: false, default: "" },
|
|
60
|
+
disabled: { type: Boolean, required: false, default: false },
|
|
61
|
+
loading: { type: Boolean, required: false, default: false },
|
|
62
|
+
submitText: { type: String, required: false, default: "\u0E2A\u0E48\u0E07" },
|
|
63
|
+
cancelText: { type: String, required: false, default: "\u0E22\u0E01\u0E40\u0E25\u0E34\u0E01" },
|
|
64
|
+
actionsOnFocus: { type: Boolean, required: false, default: false },
|
|
65
|
+
autofocus: { type: Boolean, required: false, default: false },
|
|
66
|
+
disabledActions: { type: Boolean, required: false, default: false },
|
|
67
|
+
class: { type: null, required: false }
|
|
14
68
|
});
|
|
15
|
-
const
|
|
69
|
+
const emit = defineEmits(["submit", "cancel"]);
|
|
70
|
+
const modelValue = defineModel({ type: String, ...{ default: "" } });
|
|
71
|
+
const replyTo = defineModel("replyTo", { type: null });
|
|
16
72
|
const editorRef = ref(null);
|
|
73
|
+
const isFocused = ref(false);
|
|
74
|
+
const isLocked = computed(() => props.disabled || props.loading);
|
|
75
|
+
const isEmpty = computed(() => !modelValue.value.trim() && !replyTo.value);
|
|
76
|
+
const isActionsVisible = computed(() => {
|
|
77
|
+
if (props.disabledActions) return false;
|
|
78
|
+
return !props.actionsOnFocus || isFocused.value;
|
|
79
|
+
});
|
|
80
|
+
const buildMentionChip = (name) => {
|
|
81
|
+
const chip = document.createElement("span");
|
|
82
|
+
chip.dataset.type = "mention";
|
|
83
|
+
chip.contentEditable = "false";
|
|
84
|
+
chip.className = "inline-flex items-center text-primary font-body-large-prominent select-none mr-1";
|
|
85
|
+
chip.textContent = name;
|
|
86
|
+
return chip;
|
|
87
|
+
};
|
|
88
|
+
const readText = (el) => {
|
|
89
|
+
const nodes = Array.from(el.childNodes);
|
|
90
|
+
while (nodes.length && nodes.at(-1)?.nodeName === "BR") nodes.pop();
|
|
91
|
+
let text = "";
|
|
92
|
+
nodes.forEach((node) => {
|
|
93
|
+
if (node.dataset?.type === "mention") return;
|
|
94
|
+
if (node.nodeName === "BR") {
|
|
95
|
+
text += "\n";
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (node.nodeName === "DIV") {
|
|
99
|
+
text += "\n" + node.innerText;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
text += node.textContent ?? "";
|
|
103
|
+
});
|
|
104
|
+
return text.replace(/\n{3,}/g, "\n\n").replace(/^\u00A0/, "").trim();
|
|
105
|
+
};
|
|
106
|
+
const render = () => {
|
|
107
|
+
const el = editorRef.value;
|
|
108
|
+
if (!el) return;
|
|
109
|
+
el.innerHTML = "";
|
|
110
|
+
if (replyTo.value) {
|
|
111
|
+
el.appendChild(buildMentionChip(replyTo.value.name));
|
|
112
|
+
el.appendChild(document.createTextNode("\xA0"));
|
|
113
|
+
}
|
|
114
|
+
modelValue.value.split("\n").forEach((line, i) => {
|
|
115
|
+
if (i) el.appendChild(document.createElement("br"));
|
|
116
|
+
if (line) el.appendChild(document.createTextNode(line));
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
const focusEnd = () => {
|
|
120
|
+
const el = editorRef.value;
|
|
121
|
+
if (!el) return;
|
|
122
|
+
const range = document.createRange();
|
|
123
|
+
range.selectNodeContents(el);
|
|
124
|
+
range.collapse(false);
|
|
125
|
+
const sel = window.getSelection();
|
|
126
|
+
sel?.removeAllRanges();
|
|
127
|
+
sel?.addRange(range);
|
|
128
|
+
};
|
|
129
|
+
const chipName = (el) => el.querySelector('[data-type="mention"]')?.textContent ?? void 0;
|
|
130
|
+
const onInput = () => {
|
|
131
|
+
const el = editorRef.value;
|
|
132
|
+
if (!el) return;
|
|
133
|
+
isFocused.value = true;
|
|
134
|
+
if (replyTo.value && !chipName(el)) replyTo.value = void 0;
|
|
135
|
+
modelValue.value = readText(el);
|
|
136
|
+
};
|
|
17
137
|
onMounted(() => {
|
|
18
|
-
|
|
19
|
-
|
|
138
|
+
render();
|
|
139
|
+
if (props.autofocus) nextTick(() => focus());
|
|
140
|
+
});
|
|
141
|
+
watch([modelValue, replyTo], ([, user], [, prevUser]) => {
|
|
142
|
+
const el = editorRef.value;
|
|
143
|
+
if (!el) return;
|
|
144
|
+
if (readText(el) === modelValue.value && chipName(el) === user?.name) return;
|
|
145
|
+
render();
|
|
146
|
+
const justMentioned = !!user && user !== prevUser;
|
|
147
|
+
if (justMentioned || document.activeElement === el) {
|
|
148
|
+
nextTick(() => {
|
|
149
|
+
el.focus();
|
|
150
|
+
focusEnd();
|
|
151
|
+
});
|
|
20
152
|
}
|
|
21
153
|
});
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
modelValue.value
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
154
|
+
const onEnter = () => {
|
|
155
|
+
if (isLocked.value || !modelValue.value.trim()) return;
|
|
156
|
+
emit("submit", modelValue.value);
|
|
157
|
+
};
|
|
158
|
+
const onCancel = () => {
|
|
159
|
+
modelValue.value = "";
|
|
160
|
+
replyTo.value = void 0;
|
|
161
|
+
isFocused.value = false;
|
|
162
|
+
emit("cancel");
|
|
163
|
+
};
|
|
164
|
+
const onInsertLineBreak = (event) => {
|
|
165
|
+
if (isLocked.value) return;
|
|
166
|
+
const el = event.target;
|
|
167
|
+
const sel = window.getSelection();
|
|
168
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
169
|
+
const range = sel.getRangeAt(0);
|
|
170
|
+
range.deleteContents();
|
|
171
|
+
const br = document.createElement("br");
|
|
172
|
+
range.insertNode(br);
|
|
173
|
+
if (!br.nextSibling || br.nextSibling.nodeName !== "BR" && br.nextSibling.textContent === "") {
|
|
174
|
+
const phantom = document.createElement("br");
|
|
175
|
+
br.after(phantom);
|
|
33
176
|
}
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
177
|
+
range.setStartAfter(br);
|
|
178
|
+
range.collapse(true);
|
|
179
|
+
sel.removeAllRanges();
|
|
180
|
+
sel.addRange(range);
|
|
181
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
182
|
+
};
|
|
183
|
+
const focus = () => {
|
|
184
|
+
isFocused.value = true;
|
|
185
|
+
editorRef.value?.focus();
|
|
186
|
+
focusEnd();
|
|
187
|
+
};
|
|
188
|
+
const reset = () => {
|
|
189
|
+
modelValue.value = "";
|
|
190
|
+
replyTo.value = void 0;
|
|
191
|
+
isFocused.value = false;
|
|
192
|
+
};
|
|
193
|
+
defineExpose({
|
|
194
|
+
focus,
|
|
195
|
+
reset
|
|
196
|
+
});
|
|
43
197
|
</script>
|
|
@@ -1,13 +1,58 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HTMLAttributes } from "vue";
|
|
2
|
+
import type { User } from "#pukaad-ui/types/components/comment.d.ts";
|
|
3
|
+
export interface InputCommentProps {
|
|
4
|
+
/** อวาตาร์ของคนที่กำลังพิมพ์ — ส่ง undefined ได้ Avatar จะขึ้นตัวแทน */
|
|
5
|
+
avatar?: string;
|
|
6
|
+
/** ไม่ต้องวาดอวาตาร์ — ใช้ตอนที่แถวข้างนอกมีอวาตาร์อยู่แล้ว (กล่องแก้ไขของ Comment) */
|
|
7
|
+
disabledAvatar?: boolean;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** กำลังส่ง — ล็อกช่องพิมพ์ + spinner ที่ปุ่มส่ง */
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
submitText?: string;
|
|
13
|
+
cancelText?: string;
|
|
14
|
+
/** ซ่อนปุ่มไว้ก่อน โผล่ตอนคลิกเข้าช่องพิมพ์ (กล่องคอมเมนต์หลักของ Comment) */
|
|
15
|
+
actionsOnFocus?: boolean;
|
|
16
|
+
/** โฟกัส + วาง caret ท้ายข้อความทันทีที่ mount — สำหรับกล่องที่โผล่จากการกดปุ่ม (ตอบกลับ/แก้ไข) */
|
|
17
|
+
autofocus?: boolean;
|
|
18
|
+
/** ไม่ต้องมีปุ่ม — ให้ parent คุมเอง */
|
|
19
|
+
disabledActions?: boolean;
|
|
20
|
+
class?: HTMLAttributes["class"];
|
|
21
|
+
}
|
|
2
22
|
type __VLS_Props = InputCommentProps;
|
|
3
23
|
type __VLS_ModelProps = {
|
|
4
|
-
|
|
24
|
+
/** ข้อความล้วน — ไม่รวมชื่อคนที่ตอบกลับ (ชิป mention ถูกข้ามตอนอ่าน) */
|
|
25
|
+
modelValue?: string;
|
|
26
|
+
/**
|
|
27
|
+
* คนที่ถูกตอบกลับ → แสดงเป็นชิปสี primary หน้าข้อความ
|
|
28
|
+
* เป็น v-model เพราะผู้ใช้ลบชิปทิ้งได้ แล้วคอมโพเนนต์ต้องบอก parent กลับ
|
|
29
|
+
*/
|
|
30
|
+
"replyTo"?: User | undefined;
|
|
5
31
|
};
|
|
6
32
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
7
|
-
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
8
|
-
|
|
33
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
34
|
+
focus: () => void;
|
|
35
|
+
reset: () => void;
|
|
36
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
37
|
+
submit: (value: string) => any;
|
|
38
|
+
cancel: () => any;
|
|
39
|
+
"update:modelValue": (value: string) => any;
|
|
40
|
+
"update:replyTo": (value: User | undefined) => any;
|
|
9
41
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
10
|
-
|
|
11
|
-
|
|
42
|
+
onSubmit?: ((value: string) => any) | undefined;
|
|
43
|
+
onCancel?: (() => any) | undefined;
|
|
44
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
45
|
+
"onUpdate:replyTo"?: ((value: User | undefined) => any) | undefined;
|
|
46
|
+
}>, {
|
|
47
|
+
loading: boolean;
|
|
48
|
+
disabled: boolean;
|
|
49
|
+
placeholder: string;
|
|
50
|
+
disabledAvatar: boolean;
|
|
51
|
+
submitText: string;
|
|
52
|
+
cancelText: string;
|
|
53
|
+
actionsOnFocus: boolean;
|
|
54
|
+
autofocus: boolean;
|
|
55
|
+
disabledActions: boolean;
|
|
56
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
12
57
|
declare const _default: typeof __VLS_export;
|
|
13
58
|
export default _default;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "vue";
|
|
2
|
+
export type LabelCardContentOverflow = "slide" | "ellipsis";
|
|
3
|
+
export type LabelCardContentItem = {
|
|
4
|
+
text: string;
|
|
5
|
+
color: string;
|
|
6
|
+
isSpecial?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export interface LabelCardContentProps {
|
|
9
|
+
class?: HTMLAttributes["class"];
|
|
10
|
+
items?: LabelCardContentItem | LabelCardContentItem[];
|
|
11
|
+
overflow?: LabelCardContentOverflow;
|
|
12
|
+
clickable?: boolean;
|
|
13
|
+
}
|
|
14
|
+
declare const __VLS_export: import("vue").DefineComponent<LabelCardContentProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
|
+
click: (item: LabelCardContentItem) => any;
|
|
16
|
+
}, string, import("vue").PublicProps, Readonly<LabelCardContentProps> & Readonly<{
|
|
17
|
+
onClick?: ((item: LabelCardContentItem) => any) | undefined;
|
|
18
|
+
}>, {
|
|
19
|
+
items: LabelCardContentItem | LabelCardContentItem[];
|
|
20
|
+
overflow: LabelCardContentOverflow;
|
|
21
|
+
clickable: boolean;
|
|
22
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
23
|
+
declare const _default: typeof __VLS_export;
|
|
24
|
+
export default _default;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="['relative', props.class]">
|
|
3
|
+
<div
|
|
4
|
+
ref="scroller"
|
|
5
|
+
class="flex items-center gap-[8px] scroll-smooth [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
6
|
+
:class="isEllipsis ? 'overflow-hidden' : 'overflow-x-auto'"
|
|
7
|
+
@scroll="syncArrows"
|
|
8
|
+
>
|
|
9
|
+
<div
|
|
10
|
+
v-for="(item, index) in labels"
|
|
11
|
+
:key="`${item.text}-${index}`"
|
|
12
|
+
:class="[
|
|
13
|
+
'inline-flex items-center rounded-[2px] border-2 border-solid px-[6px] py-[2px] font-title-small-prominent',
|
|
14
|
+
item.isSpecial === false ? 'bg-white' : 'text-white',
|
|
15
|
+
isEllipsis ? 'min-w-0' : 'shrink-0',
|
|
16
|
+
props.clickable && 'cursor-pointer'
|
|
17
|
+
]"
|
|
18
|
+
:style="itemStyles(item)"
|
|
19
|
+
@click="props.clickable && emit('click', item)"
|
|
20
|
+
>
|
|
21
|
+
<span class="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">{{
|
|
22
|
+
item.text
|
|
23
|
+
}}</span>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<template v-if="canSlide">
|
|
28
|
+
<div
|
|
29
|
+
v-if="canScrollLeft"
|
|
30
|
+
class="absolute top-0 left-0 flex h-[32px] w-[50px] items-center bg-gradient-to-r from-bright from-50% to-transparent pl-[4px]"
|
|
31
|
+
>
|
|
32
|
+
<Button
|
|
33
|
+
variant="icon"
|
|
34
|
+
circle
|
|
35
|
+
class="size-[24px] hover:bg-mercury hover:drop-shadow"
|
|
36
|
+
@click="slide(-1)"
|
|
37
|
+
>
|
|
38
|
+
<Icon name="lucide:chevron-left" size="16" />
|
|
39
|
+
</Button>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div
|
|
43
|
+
v-if="canScrollRight"
|
|
44
|
+
class="absolute top-0 right-0 flex h-[32px] w-[50px] items-center justify-end bg-gradient-to-l from-bright from-50% to-transparent pr-[4px]"
|
|
45
|
+
>
|
|
46
|
+
<Button
|
|
47
|
+
variant="icon"
|
|
48
|
+
circle
|
|
49
|
+
class="size-[24px] hover:bg-mercury hover:drop-shadow"
|
|
50
|
+
@click="slide(1)"
|
|
51
|
+
>
|
|
52
|
+
<Icon name="lucide:chevron-right" size="16" />
|
|
53
|
+
</Button>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<script setup>
|
|
60
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
61
|
+
const props = defineProps({
|
|
62
|
+
class: { type: null, required: false },
|
|
63
|
+
items: { type: [Object, Array], required: false, default: () => [] },
|
|
64
|
+
overflow: { type: String, required: false, default: "slide" },
|
|
65
|
+
clickable: { type: Boolean, required: false, default: false }
|
|
66
|
+
});
|
|
67
|
+
const emit = defineEmits(["click"]);
|
|
68
|
+
const labels = computed(
|
|
69
|
+
() => Array.isArray(props.items) ? props.items : [props.items]
|
|
70
|
+
);
|
|
71
|
+
const isEllipsis = computed(() => props.overflow === "ellipsis");
|
|
72
|
+
const canSlide = computed(() => !isEllipsis.value);
|
|
73
|
+
const itemStyles = (item) => {
|
|
74
|
+
const color = item.color?.startsWith("#") ? item.color : `#${item.color}`;
|
|
75
|
+
return item.isSpecial === false ? { color, borderColor: color } : { backgroundColor: color, borderColor: color };
|
|
76
|
+
};
|
|
77
|
+
const scroller = ref(null);
|
|
78
|
+
const canScrollLeft = ref(false);
|
|
79
|
+
const canScrollRight = ref(false);
|
|
80
|
+
const SCROLL_EPSILON = 1;
|
|
81
|
+
const syncArrows = () => {
|
|
82
|
+
const element = scroller.value;
|
|
83
|
+
if (!element) return;
|
|
84
|
+
canScrollLeft.value = element.scrollLeft > SCROLL_EPSILON;
|
|
85
|
+
canScrollRight.value = element.scrollLeft + element.clientWidth < element.scrollWidth - SCROLL_EPSILON;
|
|
86
|
+
};
|
|
87
|
+
const slide = (direction) => {
|
|
88
|
+
const element = scroller.value;
|
|
89
|
+
if (!element) return;
|
|
90
|
+
element.scrollBy({
|
|
91
|
+
left: direction * element.clientWidth * 0.8,
|
|
92
|
+
behavior: "smooth"
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
let observer;
|
|
96
|
+
onMounted(() => {
|
|
97
|
+
syncArrows();
|
|
98
|
+
if (scroller.value && typeof ResizeObserver !== "undefined") {
|
|
99
|
+
observer = new ResizeObserver(syncArrows);
|
|
100
|
+
observer.observe(scroller.value);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
onBeforeUnmount(() => observer?.disconnect());
|
|
104
|
+
watch(labels, syncArrows, { flush: "post" });
|
|
105
|
+
</script>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "vue";
|
|
2
|
+
export type LabelCardContentOverflow = "slide" | "ellipsis";
|
|
3
|
+
export type LabelCardContentItem = {
|
|
4
|
+
text: string;
|
|
5
|
+
color: string;
|
|
6
|
+
isSpecial?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export interface LabelCardContentProps {
|
|
9
|
+
class?: HTMLAttributes["class"];
|
|
10
|
+
items?: LabelCardContentItem | LabelCardContentItem[];
|
|
11
|
+
overflow?: LabelCardContentOverflow;
|
|
12
|
+
clickable?: boolean;
|
|
13
|
+
}
|
|
14
|
+
declare const __VLS_export: import("vue").DefineComponent<LabelCardContentProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
|
+
click: (item: LabelCardContentItem) => any;
|
|
16
|
+
}, string, import("vue").PublicProps, Readonly<LabelCardContentProps> & Readonly<{
|
|
17
|
+
onClick?: ((item: LabelCardContentItem) => any) | undefined;
|
|
18
|
+
}>, {
|
|
19
|
+
items: LabelCardContentItem | LabelCardContentItem[];
|
|
20
|
+
overflow: LabelCardContentOverflow;
|
|
21
|
+
clickable: boolean;
|
|
22
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
23
|
+
declare const _default: typeof __VLS_export;
|
|
24
|
+
export default _default;
|
|
@@ -5,9 +5,12 @@ declare const __VLS_export: import("vue").DefineComponent<PickerOptionMenuUserPr
|
|
|
5
5
|
"blog-unpin": () => any;
|
|
6
6
|
"blog-pin": () => any;
|
|
7
7
|
"blog-delete": () => any;
|
|
8
|
+
"reply-delete": () => any;
|
|
8
9
|
"report-review": () => any;
|
|
9
10
|
"review-edit": () => any;
|
|
10
11
|
"review-delete": () => any;
|
|
12
|
+
"review-reply": () => any;
|
|
13
|
+
"reply-edit": () => any;
|
|
11
14
|
"profile-name-updated": (name: string) => any;
|
|
12
15
|
"profile-edit": () => any;
|
|
13
16
|
"profile-share": () => any;
|
|
@@ -24,9 +27,12 @@ declare const __VLS_export: import("vue").DefineComponent<PickerOptionMenuUserPr
|
|
|
24
27
|
"onBlog-unpin"?: (() => any) | undefined;
|
|
25
28
|
"onBlog-pin"?: (() => any) | undefined;
|
|
26
29
|
"onBlog-delete"?: (() => any) | undefined;
|
|
30
|
+
"onReply-delete"?: (() => any) | undefined;
|
|
27
31
|
"onReport-review"?: (() => any) | undefined;
|
|
28
32
|
"onReview-edit"?: (() => any) | undefined;
|
|
29
33
|
"onReview-delete"?: (() => any) | undefined;
|
|
34
|
+
"onReview-reply"?: (() => any) | undefined;
|
|
35
|
+
"onReply-edit"?: (() => any) | undefined;
|
|
30
36
|
"onProfile-name-updated"?: ((name: string) => any) | undefined;
|
|
31
37
|
"onProfile-edit"?: (() => any) | undefined;
|
|
32
38
|
"onProfile-share"?: (() => any) | undefined;
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
<script setup>
|
|
23
23
|
import { computed, ref } from "vue";
|
|
24
|
-
const emit = defineEmits(["profile-edit", "profile-share", "profile-follower-delete", "profile-block", "blog-pin", "blog-unpin", "blog-edit", "blog-delete", "report-blog", "report-place", "report-profile", "report-announce", "report-review", "place-close-duplicate", "review-edit", "review-delete", "archive", "profile-name-updated"]);
|
|
24
|
+
const emit = defineEmits(["profile-edit", "profile-share", "profile-follower-delete", "profile-block", "blog-pin", "blog-unpin", "blog-edit", "blog-delete", "report-blog", "report-place", "report-profile", "report-announce", "report-review", "place-close-duplicate", "review-edit", "review-delete", "review-reply", "reply-edit", "reply-delete", "archive", "profile-name-updated"]);
|
|
25
25
|
const props = defineProps({
|
|
26
26
|
state: { type: String, required: false },
|
|
27
27
|
profileDetail: { type: Object, required: false },
|
|
@@ -162,6 +162,16 @@ const menu = [
|
|
|
162
162
|
emit("report-announce");
|
|
163
163
|
}
|
|
164
164
|
},
|
|
165
|
+
// ลำดับใน array นี้ = ลำดับที่แสดงในเมนู (items ใช้ menu.filter ซึ่งคงลำดับเดิมไว้)
|
|
166
|
+
// "ตอบกลับ" ต้องอยู่เหนือ "รายงานรีวิวนี้" ตาม Figma 2456:51570
|
|
167
|
+
{
|
|
168
|
+
label: "\u0E15\u0E2D\u0E1A\u0E01\u0E25\u0E31\u0E1A",
|
|
169
|
+
name: "review-reply",
|
|
170
|
+
icon: "lucide:reply",
|
|
171
|
+
action: () => {
|
|
172
|
+
emit("review-reply");
|
|
173
|
+
}
|
|
174
|
+
},
|
|
165
175
|
{
|
|
166
176
|
label: "\u0E23\u0E32\u0E22\u0E07\u0E32\u0E19\u0E23\u0E35\u0E27\u0E34\u0E27\u0E19\u0E35\u0E49",
|
|
167
177
|
name: "report-review",
|
|
@@ -187,6 +197,23 @@ const menu = [
|
|
|
187
197
|
action: () => {
|
|
188
198
|
emit("review-delete");
|
|
189
199
|
}
|
|
200
|
+
},
|
|
201
|
+
// เมนูของกล่อง "การตอบกลับจาก ..." ที่ธุรกิจตอบไว้ (Figma 2456:88751)
|
|
202
|
+
{
|
|
203
|
+
label: "\u0E41\u0E01\u0E49\u0E44\u0E02",
|
|
204
|
+
name: "reply-edit",
|
|
205
|
+
icon: "lucide:square-pen",
|
|
206
|
+
action: () => {
|
|
207
|
+
emit("reply-edit");
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
label: "\u0E25\u0E1A",
|
|
212
|
+
name: "reply-delete",
|
|
213
|
+
icon: "lucide:trash-2",
|
|
214
|
+
action: () => {
|
|
215
|
+
emit("reply-delete");
|
|
216
|
+
}
|
|
190
217
|
}
|
|
191
218
|
];
|
|
192
219
|
const items = computed(() => {
|
|
@@ -242,6 +269,16 @@ const items = computed(() => {
|
|
|
242
269
|
(item) => ["report-review"].includes(item.name)
|
|
243
270
|
);
|
|
244
271
|
}
|
|
272
|
+
if (props.state === "business-review") {
|
|
273
|
+
return menu.filter(
|
|
274
|
+
(item) => ["review-reply", "report-review"].includes(item.name)
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (props.state === "my-reply") {
|
|
278
|
+
return menu.filter(
|
|
279
|
+
(item) => ["reply-edit", "reply-delete"].includes(item.name)
|
|
280
|
+
);
|
|
281
|
+
}
|
|
245
282
|
return menu;
|
|
246
283
|
});
|
|
247
284
|
</script>
|
|
@@ -5,9 +5,12 @@ declare const __VLS_export: import("vue").DefineComponent<PickerOptionMenuUserPr
|
|
|
5
5
|
"blog-unpin": () => any;
|
|
6
6
|
"blog-pin": () => any;
|
|
7
7
|
"blog-delete": () => any;
|
|
8
|
+
"reply-delete": () => any;
|
|
8
9
|
"report-review": () => any;
|
|
9
10
|
"review-edit": () => any;
|
|
10
11
|
"review-delete": () => any;
|
|
12
|
+
"review-reply": () => any;
|
|
13
|
+
"reply-edit": () => any;
|
|
11
14
|
"profile-name-updated": (name: string) => any;
|
|
12
15
|
"profile-edit": () => any;
|
|
13
16
|
"profile-share": () => any;
|
|
@@ -24,9 +27,12 @@ declare const __VLS_export: import("vue").DefineComponent<PickerOptionMenuUserPr
|
|
|
24
27
|
"onBlog-unpin"?: (() => any) | undefined;
|
|
25
28
|
"onBlog-pin"?: (() => any) | undefined;
|
|
26
29
|
"onBlog-delete"?: (() => any) | undefined;
|
|
30
|
+
"onReply-delete"?: (() => any) | undefined;
|
|
27
31
|
"onReport-review"?: (() => any) | undefined;
|
|
28
32
|
"onReview-edit"?: (() => any) | undefined;
|
|
29
33
|
"onReview-delete"?: (() => any) | undefined;
|
|
34
|
+
"onReview-reply"?: (() => any) | undefined;
|
|
35
|
+
"onReply-edit"?: (() => any) | undefined;
|
|
30
36
|
"onProfile-name-updated"?: ((name: string) => any) | undefined;
|
|
31
37
|
"onProfile-edit"?: (() => any) | undefined;
|
|
32
38
|
"onProfile-share"?: (() => any) | undefined;
|
|
@@ -22,7 +22,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
|
|
|
22
22
|
onOpenAutoFocus?: ((event: Event) => any) | undefined;
|
|
23
23
|
onCloseAutoFocus?: ((event: Event) => any) | undefined;
|
|
24
24
|
}>, {
|
|
25
|
-
align: "
|
|
25
|
+
align: "start" | "center" | "end";
|
|
26
26
|
sideOffset: number;
|
|
27
27
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
28
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -22,7 +22,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
|
|
|
22
22
|
onOpenAutoFocus?: ((event: Event) => any) | undefined;
|
|
23
23
|
onCloseAutoFocus?: ((event: Event) => any) | undefined;
|
|
24
24
|
}>, {
|
|
25
|
-
align: "
|
|
25
|
+
align: "start" | "center" | "end";
|
|
26
26
|
sideOffset: number;
|
|
27
27
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
28
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
package/package.json
CHANGED
|
@@ -1,3 +0,0 @@
|
|
|
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>;
|
|
2
|
-
declare const _default: typeof __VLS_export;
|
|
3
|
-
export default _default;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!-- <div :class="['flex py-[2px]', state, color]">
|
|
3
|
-
<div class="font-body-extra-small text-word-on-surface">
|
|
4
|
-
{{ props.text }}
|
|
5
|
-
</div>
|
|
6
|
-
</div> -->
|
|
7
|
-
</template>
|
|
8
|
-
|
|
9
|
-
<script setup>
|
|
10
|
-
|
|
11
|
-
</script>
|
|
12
|
-
|
|
13
|
-
<style scoped>
|
|
14
|
-
/* .state-last {
|
|
15
|
-
// clip-path: "polygon(0 0, 100% 0, 90% 100%, 0 100%)";
|
|
16
|
-
// } */
|
|
17
|
-
</style>
|
|
@@ -1,3 +0,0 @@
|
|
|
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>;
|
|
2
|
-
declare const _default: typeof __VLS_export;
|
|
3
|
-
export default _default;
|