contactstudiocstools 1.0.224
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/README.md +94 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.ts +7 -0
- package/dist/module.json +5 -0
- package/dist/module.mjs +72 -0
- package/dist/runtime/components/Atom.Alert.vue +46 -0
- package/dist/runtime/components/Atom.Auth.vue +37 -0
- package/dist/runtime/components/Atom.BannerChatEmpty.vue +18 -0
- package/dist/runtime/components/Atom.BannerPage404.vue +28 -0
- package/dist/runtime/components/Atom.BannerPageUnauthorized.vue +18 -0
- package/dist/runtime/components/Atom.Breadcrumb.vue +26 -0
- package/dist/runtime/components/Atom.ChatContact.vue +136 -0
- package/dist/runtime/components/Atom.ChatContactSchedule.vue +87 -0
- package/dist/runtime/components/Atom.ChatMessageFooter.vue +25 -0
- package/dist/runtime/components/Atom.DarkMode.vue +67 -0
- package/dist/runtime/components/Atom.DraggableWindow.vue +102 -0
- package/dist/runtime/components/Atom.Dropdown.vue +9 -0
- package/dist/runtime/components/Atom.DropdownSearchable.vue +25 -0
- package/dist/runtime/components/Atom.Fetch.vue +46 -0
- package/dist/runtime/components/Atom.Field.vue +43 -0
- package/dist/runtime/components/Atom.FieldDate.vue +19 -0
- package/dist/runtime/components/Atom.FieldNumber.vue +19 -0
- package/dist/runtime/components/Atom.FieldPhone.vue +92 -0
- package/dist/runtime/components/Atom.FieldSelect.vue +28 -0
- package/dist/runtime/components/Atom.FieldSelectMultiple.vue +49 -0
- package/dist/runtime/components/Atom.FieldText.vue +19 -0
- package/dist/runtime/components/Atom.FieldTextarea.vue +41 -0
- package/dist/runtime/components/Atom.Loading.vue +80 -0
- package/dist/runtime/components/Atom.Notification.vue +48 -0
- package/dist/runtime/components/Atom.Ringtone.vue +23 -0
- package/dist/runtime/components/Atom.SelectTreeField.vue +49 -0
- package/dist/runtime/components/Atom.Snapshot.vue +33 -0
- package/dist/runtime/components/Atom.Tabs.vue +60 -0
- package/dist/runtime/components/Molecule.ChatMessageFile.vue +102 -0
- package/dist/runtime/components/Molecule.ChatMessageOption.vue +85 -0
- package/dist/runtime/components/Molecule.ChatMessageText.vue +36 -0
- package/dist/runtime/components/Molecule.ClientHistory.vue +62 -0
- package/dist/runtime/components/Molecule.DropdownDDI.vue +333 -0
- package/dist/runtime/components/Molecule.FieldGroup.vue +73 -0
- package/dist/runtime/components/Molecule.FieldSelectMultiple.vue +19 -0
- package/dist/runtime/components/Molecule.File.vue +84 -0
- package/dist/runtime/components/Molecule.SelectTreeSearchable.vue +126 -0
- package/dist/runtime/components/Molecule.Status.vue +154 -0
- package/dist/runtime/components/Molecule.TimeDaily.vue +9 -0
- package/dist/runtime/components/Organism.Attachments.vue +139 -0
- package/dist/runtime/components/Organism.ChatMessages.vue +31 -0
- package/dist/runtime/components/Organism.ChatRoom.vue +342 -0
- package/dist/runtime/components/Organism.ChatSchedule.vue +110 -0
- package/dist/runtime/components/Organism.ClientHistoryTable.vue +85 -0
- package/dist/runtime/components/Organism.ClientHistoryTimeline.vue +77 -0
- package/dist/runtime/components/Organism.FAQ.vue +88 -0
- package/dist/runtime/components/Organism.Form.vue +67 -0
- package/dist/runtime/components/Organism.FormMailing.vue +112 -0
- package/dist/runtime/components/Organism.HeaderMain.vue +79 -0
- package/dist/runtime/components/Organism.Manifestation.vue +146 -0
- package/dist/runtime/components/Organism.Nav.vue +27 -0
- package/dist/runtime/components/Organism.NavMain.vue +187 -0
- package/dist/runtime/components/Organism.PageContainer.vue +22 -0
- package/dist/runtime/components/Organism.Schedule.vue +170 -0
- package/dist/runtime/components/Organism.Tabulation.vue +237 -0
- package/dist/runtime/components/types/dto.d.ts +16 -0
- package/dist/runtime/components/types/dto.mjs +236 -0
- package/dist/runtime/components/types/helpers.d.ts +39 -0
- package/dist/runtime/components/types/helpers.mjs +295 -0
- package/dist/runtime/components/types/index.d.ts +4 -0
- package/dist/runtime/components/types/index.mjs +4 -0
- package/dist/runtime/components/types/types.d.ts +198 -0
- package/dist/runtime/components/types/types.mjs +35 -0
- package/dist/runtime/index.css +1 -0
- package/dist/runtime/plugins/clickOutside.d.ts +2 -0
- package/dist/runtime/plugins/clickOutside.mjs +16 -0
- package/dist/runtime/plugins/emitter.d.ts +2 -0
- package/dist/runtime/plugins/emitter.mjs +17 -0
- package/dist/runtime/public/192x192.png +0 -0
- package/dist/runtime/public/404.svg +1 -0
- package/dist/runtime/public/512x512.png +0 -0
- package/dist/runtime/public/chat.svg +138 -0
- package/dist/runtime/public/chatbg.png +0 -0
- package/dist/runtime/public/dev-sw.d.ts +0 -0
- package/dist/runtime/public/dev-sw.mjs +0 -0
- package/dist/runtime/public/empty.svg +1 -0
- package/dist/runtime/public/loading.svg +1 -0
- package/dist/runtime/public/messages.svg +1 -0
- package/dist/runtime/public/privacy.svg +1 -0
- package/dist/runtime/public/ringtone.mp3 +0 -0
- package/dist/runtime/public/security.svg +188 -0
- package/dist/runtime/public/snapshot.d.ts +15 -0
- package/dist/runtime/public/snapshot.mjs +77 -0
- package/dist/runtime/public/unauthorized.svg +1 -0
- package/dist/types.d.ts +10 -0
- package/package.json +50 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:disabled="disabled"
|
|
4
|
+
class="w-4 bg-slate-700 rounded-full"
|
|
5
|
+
>
|
|
6
|
+
<!-- current -->
|
|
7
|
+
<div
|
|
8
|
+
:class="getStatusColor(current)"
|
|
9
|
+
class="flex justify-end rounded-full dark"
|
|
10
|
+
>
|
|
11
|
+
<p class="text-xs mr-2 whitespace-nowrap max-sm:hidden">
|
|
12
|
+
<span
|
|
13
|
+
:class="getStatusColor(current)"
|
|
14
|
+
class="badge bg-transparent badge-outline font-bold"
|
|
15
|
+
>
|
|
16
|
+
{{ new Date(milliseconds).toISOString().slice(11, 19) }}
|
|
17
|
+
</span>
|
|
18
|
+
{{ getStatusLabel(current) }}
|
|
19
|
+
</p>
|
|
20
|
+
<p
|
|
21
|
+
:dropdown-trigger="dropdownID"
|
|
22
|
+
class="sizes cursor-pointer flex justify-center items-center"
|
|
23
|
+
>
|
|
24
|
+
<i
|
|
25
|
+
class="bi-caret-down-fill !text-[11px] mt-0.5 text-white pointer-events-none"
|
|
26
|
+
/>
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="my-2" />
|
|
31
|
+
|
|
32
|
+
<!-- next -->
|
|
33
|
+
<div class="flex justify-end">
|
|
34
|
+
<span
|
|
35
|
+
:class="getStatusColor(next)"
|
|
36
|
+
class="text-xs mr-2 !bg-transparent max-sm:hidden"
|
|
37
|
+
v-text="getStatusLabel(next)"
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
<p class="sizes rounded-full flex justify-center items-center bottom-0">
|
|
41
|
+
<span
|
|
42
|
+
:class="getStatusColor(next)"
|
|
43
|
+
class="w-2 h-2 rounded-full"
|
|
44
|
+
/>
|
|
45
|
+
</p>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- dropdown -->
|
|
49
|
+
<AtomDropdown
|
|
50
|
+
:dropdown="dropdownID"
|
|
51
|
+
class="!p-1"
|
|
52
|
+
>
|
|
53
|
+
<li
|
|
54
|
+
:class="getStatusColor(current)"
|
|
55
|
+
class="text-xs !text-[11px] !bg-transparent !p-2 !py-0.5 sm:hidden"
|
|
56
|
+
>
|
|
57
|
+
<i class="bi-caret-down-fill text-[10px]" />
|
|
58
|
+
{{ getStatusLabel(current) }}
|
|
59
|
+
</li>
|
|
60
|
+
|
|
61
|
+
<li
|
|
62
|
+
:class="getStatusColor(next)"
|
|
63
|
+
class="text-xs !text-[11px] !bg-transparent !p-2 !py-0.5 sm:hidden"
|
|
64
|
+
>
|
|
65
|
+
<i class="bi-circle-fill text-[6px] px-0.5" />
|
|
66
|
+
{{ getStatusLabel(next) }}
|
|
67
|
+
</li>
|
|
68
|
+
|
|
69
|
+
<div class="divider sm:hidden" />
|
|
70
|
+
|
|
71
|
+
<li
|
|
72
|
+
v-for="(item, i) in pauses"
|
|
73
|
+
:key="i"
|
|
74
|
+
class="item text-xs !p-2"
|
|
75
|
+
@click="set(item)"
|
|
76
|
+
v-text="item.label"
|
|
77
|
+
/>
|
|
78
|
+
</AtomDropdown>
|
|
79
|
+
</div>
|
|
80
|
+
</template>
|
|
81
|
+
|
|
82
|
+
<script setup lang="ts">
|
|
83
|
+
import { ref, computed, watch, onMounted } from "vue";
|
|
84
|
+
import { randomID } from "./types";
|
|
85
|
+
import { EUserStatus, IPause, IPauses } from "./types";
|
|
86
|
+
|
|
87
|
+
// props
|
|
88
|
+
interface IProps {
|
|
89
|
+
startedAt: Date;
|
|
90
|
+
current: EUserStatus;
|
|
91
|
+
next: EUserStatus;
|
|
92
|
+
pauses: IPauses;
|
|
93
|
+
pause?: IPause;
|
|
94
|
+
set: Function;
|
|
95
|
+
}
|
|
96
|
+
const props = defineProps<IProps>();
|
|
97
|
+
|
|
98
|
+
// mounted
|
|
99
|
+
onMounted(() => {
|
|
100
|
+
resetSeconds();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// computed
|
|
104
|
+
const getStatusLabel = computed<Function>(() => (status: EUserStatus) => {
|
|
105
|
+
if (status !== EUserStatus.pause) return status;
|
|
106
|
+
|
|
107
|
+
return props.pause?.label;
|
|
108
|
+
});
|
|
109
|
+
const getStatusColor = computed<Function>(() => (status: EUserStatus) => {
|
|
110
|
+
if (status === EUserStatus.active) return "bg-active text-active";
|
|
111
|
+
if (status === EUserStatus.attendance) return "bg-attendance text-attendance";
|
|
112
|
+
if (status === EUserStatus.available) return "bg-available text-available";
|
|
113
|
+
|
|
114
|
+
return "bg-pause text-pause";
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// data
|
|
118
|
+
let counter: any;
|
|
119
|
+
const dropdownID = ref<string>(randomID());
|
|
120
|
+
const disabled = ref<boolean>(false);
|
|
121
|
+
const milliseconds = ref<number>(0);
|
|
122
|
+
|
|
123
|
+
// methods
|
|
124
|
+
async function set(pause: IPause): Promise<void> {
|
|
125
|
+
disabled.value = true;
|
|
126
|
+
await props.set(pause);
|
|
127
|
+
disabled.value = false;
|
|
128
|
+
}
|
|
129
|
+
function resetSeconds(): void {
|
|
130
|
+
milliseconds.value = 0;
|
|
131
|
+
|
|
132
|
+
window.clearInterval(counter);
|
|
133
|
+
counter = window.setInterval(() => {
|
|
134
|
+
milliseconds.value = Math.abs(props.startedAt.getTime() - new Date().getTime());
|
|
135
|
+
}, 1000);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// watch
|
|
139
|
+
watch(
|
|
140
|
+
() => props.current,
|
|
141
|
+
() => {
|
|
142
|
+
resetSeconds();
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<style scoped>
|
|
148
|
+
.sizes {
|
|
149
|
+
min-width: 1rem;
|
|
150
|
+
max-width: 1rem;
|
|
151
|
+
min-height: 1rem;
|
|
152
|
+
max-height: 1rem;
|
|
153
|
+
}
|
|
154
|
+
</style>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-wrap gap-2">
|
|
3
|
+
<MoleculeFile
|
|
4
|
+
v-for="({ file, ID }, i) in attachments"
|
|
5
|
+
:key="i"
|
|
6
|
+
:file="file.file"
|
|
7
|
+
:hints="file.hints"
|
|
8
|
+
:status="file.status"
|
|
9
|
+
class="flex-1"
|
|
10
|
+
actions
|
|
11
|
+
>
|
|
12
|
+
<li
|
|
13
|
+
class="item !p-2 text-error"
|
|
14
|
+
@click="deleteAttachment(ID)"
|
|
15
|
+
>
|
|
16
|
+
<i class="bi-trash" /> Excluir
|
|
17
|
+
</li>
|
|
18
|
+
</MoleculeFile>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script setup lang="ts">
|
|
23
|
+
import { reactive, watch } from "vue";
|
|
24
|
+
import {
|
|
25
|
+
EFieldStatus,
|
|
26
|
+
IAttachment,
|
|
27
|
+
IAttachments,
|
|
28
|
+
logger,
|
|
29
|
+
randomID,
|
|
30
|
+
} from "./types";
|
|
31
|
+
import { onMounted } from "vue";
|
|
32
|
+
|
|
33
|
+
// props
|
|
34
|
+
interface IProps {
|
|
35
|
+
inputFileRef?: HTMLInputElement;
|
|
36
|
+
init: () => Promise<IAttachments>;
|
|
37
|
+
delete: (attachment: IAttachment) => Promise<void>;
|
|
38
|
+
upload: (file: File) => Promise<any>;
|
|
39
|
+
}
|
|
40
|
+
const props = defineProps<IProps>();
|
|
41
|
+
|
|
42
|
+
// mounted
|
|
43
|
+
onMounted(async () => {
|
|
44
|
+
const uploadedAttachments = await props.init();
|
|
45
|
+
|
|
46
|
+
if (!uploadedAttachments) return;
|
|
47
|
+
|
|
48
|
+
attachments.push(...uploadedAttachments);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// data
|
|
52
|
+
const attachments = reactive<IAttachments>([]);
|
|
53
|
+
|
|
54
|
+
// methods
|
|
55
|
+
function setOnChangeEvent(): void {
|
|
56
|
+
if (!props.inputFileRef) return;
|
|
57
|
+
|
|
58
|
+
props.inputFileRef.onchange = getAttachmentsOnChange;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getAttachmentsOnChange(e: Event): void {
|
|
62
|
+
const inputFiles = (e.target as HTMLInputElement).files;
|
|
63
|
+
|
|
64
|
+
if (!inputFiles) return;
|
|
65
|
+
|
|
66
|
+
for (const file of inputFiles) {
|
|
67
|
+
const ID = randomID();
|
|
68
|
+
|
|
69
|
+
attachments.push({
|
|
70
|
+
ID,
|
|
71
|
+
file: { file },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
addAttachment({ ID, file });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
resetInputFile();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function addAttachment({ ID, file }: any): Promise<void> {
|
|
81
|
+
const index = getAttachmentIndexByID(ID);
|
|
82
|
+
|
|
83
|
+
setLoadingStatusOnFile(index);
|
|
84
|
+
|
|
85
|
+
if (!props.upload) return;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const primitive = await props.upload(file);
|
|
89
|
+
setPrimitiveOnAttachment({ index, primitive });
|
|
90
|
+
setSuccessStatusOnFile(index);
|
|
91
|
+
} catch (e: any) {
|
|
92
|
+
setErrorStatusOnFile(index);
|
|
93
|
+
logger.error("OrganismAttachments:AddAttachment", { e });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getAttachmentIndexByID(ID: string): number {
|
|
98
|
+
return attachments.findIndex((attachment) => attachment.ID === ID);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resetInputFile(): void {
|
|
102
|
+
if (!props.inputFileRef) return;
|
|
103
|
+
props.inputFileRef.value = "";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function deleteAttachment(ID: string): Promise<void> {
|
|
107
|
+
const index = getAttachmentIndexByID(ID);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
setLoadingStatusOnFile(index);
|
|
111
|
+
|
|
112
|
+
await props.delete(attachments[index]);
|
|
113
|
+
|
|
114
|
+
attachments.splice(index, 1);
|
|
115
|
+
} catch (e: any) {
|
|
116
|
+
setErrorStatusOnFile(index);
|
|
117
|
+
logger.error("OrganismAttachments:deleteAttachment", { e });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function setPrimitiveOnAttachment({ index, primitive }: any): void {
|
|
122
|
+
attachments[index].primitive = primitive;
|
|
123
|
+
}
|
|
124
|
+
function setLoadingStatusOnFile(index: number): void {
|
|
125
|
+
attachments[index].file.status = EFieldStatus.loading;
|
|
126
|
+
}
|
|
127
|
+
function setSuccessStatusOnFile(index: number): void {
|
|
128
|
+
attachments[index].file.status = EFieldStatus.success;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function setErrorStatusOnFile(index: number): void {
|
|
132
|
+
attachments[index].file.status = EFieldStatus.error;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// watch
|
|
136
|
+
watch(() => props.inputFileRef, setOnChangeEvent);
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
<style scoped></style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-for="(message, i) in messages"
|
|
4
|
+
:key="i"
|
|
5
|
+
>
|
|
6
|
+
<MoleculeChatMessageText
|
|
7
|
+
v-if="message.type === EChatMessageTypes.text"
|
|
8
|
+
:message="message"
|
|
9
|
+
/>
|
|
10
|
+
<MoleculeChatMessageOption
|
|
11
|
+
v-if="message.type === EChatMessageTypes.option"
|
|
12
|
+
:message="message"
|
|
13
|
+
/>
|
|
14
|
+
<MoleculeChatMessageFile
|
|
15
|
+
v-if="message.type === EChatMessageTypes.file"
|
|
16
|
+
:message="message"
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
import { IChatMessages, EChatMessageTypes } from './types';
|
|
23
|
+
|
|
24
|
+
// props
|
|
25
|
+
interface IProps {
|
|
26
|
+
messages?: IChatMessages;
|
|
27
|
+
}
|
|
28
|
+
defineProps<IProps>();
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<style scoped></style>
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AtomBannerChatEmpty v-if="!contact" />
|
|
3
|
+
<div
|
|
4
|
+
v-else
|
|
5
|
+
class="w-full flex flex-col max-h-screen"
|
|
6
|
+
>
|
|
7
|
+
<!-- header -->
|
|
8
|
+
<header class="flex items-center h-12 px-5">
|
|
9
|
+
<!-- avatar -->
|
|
10
|
+
<figure
|
|
11
|
+
:class="[getStatusClass]"
|
|
12
|
+
class="w-10 h-10 avatar avatar-soft-secondary"
|
|
13
|
+
>
|
|
14
|
+
<img
|
|
15
|
+
v-if="contact.avatar"
|
|
16
|
+
:src="contact.avatar"
|
|
17
|
+
class="rounded-md"
|
|
18
|
+
>
|
|
19
|
+
<i
|
|
20
|
+
v-else
|
|
21
|
+
class="bi-person-fill text-xl"
|
|
22
|
+
/>
|
|
23
|
+
</figure>
|
|
24
|
+
|
|
25
|
+
<!-- info -->
|
|
26
|
+
<div class="ml-3">
|
|
27
|
+
<p
|
|
28
|
+
class="text-sm"
|
|
29
|
+
v-text="contact?.label"
|
|
30
|
+
/>
|
|
31
|
+
<p
|
|
32
|
+
v-if="contact.typing"
|
|
33
|
+
class="text-success text-xs"
|
|
34
|
+
>
|
|
35
|
+
Digitando...
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
</header>
|
|
39
|
+
|
|
40
|
+
<div class="divider" />
|
|
41
|
+
|
|
42
|
+
<!-- messages -->
|
|
43
|
+
<article
|
|
44
|
+
ref="OrganismChatMessagesRef"
|
|
45
|
+
class="scrollbar flex flex-col flex-1 overflow-y-auto p-3"
|
|
46
|
+
>
|
|
47
|
+
<div
|
|
48
|
+
v-if="!messages?.length"
|
|
49
|
+
class="w-full h-full flex justify-center items-center"
|
|
50
|
+
>
|
|
51
|
+
<span class="spin" />
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<OrganismChatMessages :messages="messages" />
|
|
55
|
+
</article>
|
|
56
|
+
|
|
57
|
+
<!-- subject -->
|
|
58
|
+
<span
|
|
59
|
+
v-if="subject"
|
|
60
|
+
:tooltip="subject"
|
|
61
|
+
class="badge badge-outline-secondary text-xs flex justify-center items-center gap-1 cursor-pointer"
|
|
62
|
+
>
|
|
63
|
+
<i class="bi-info-circle text-sm" />
|
|
64
|
+
Resumo Jornada
|
|
65
|
+
</span>
|
|
66
|
+
|
|
67
|
+
<!-- suggestions -->
|
|
68
|
+
<div
|
|
69
|
+
v-if="greetings?.length || goodbyes?.length || absents?.length"
|
|
70
|
+
class="divider"
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
<div
|
|
74
|
+
v-if="greetings?.length || goodbyes?.length || absents?.length"
|
|
75
|
+
:disabled="!contact?.on"
|
|
76
|
+
class="flex flex-wrap gap-2 overflow-x-auto scrollbar px-3 my-2"
|
|
77
|
+
>
|
|
78
|
+
<div
|
|
79
|
+
v-for="(greeting, i) in greetings"
|
|
80
|
+
:key="i"
|
|
81
|
+
:tooltip="marked(greeting)"
|
|
82
|
+
class="suggestions badge badge-solid-success rounded w-14 whitespace-nowrap cursor-pointer"
|
|
83
|
+
@click="send(greeting)"
|
|
84
|
+
v-html="marked(greeting)"
|
|
85
|
+
/>
|
|
86
|
+
<div
|
|
87
|
+
v-for="(absent, i) in absents"
|
|
88
|
+
:key="i"
|
|
89
|
+
:tooltip="marked(absent)"
|
|
90
|
+
class="suggestions badge badge-solid-secondary rounded w-14 whitespace-nowrap cursor-pointer"
|
|
91
|
+
@click="send(absent)"
|
|
92
|
+
v-html="marked(absent)"
|
|
93
|
+
/>
|
|
94
|
+
<div
|
|
95
|
+
v-for="(goodbye, i) in goodbyes"
|
|
96
|
+
:key="i"
|
|
97
|
+
:tooltip="marked(goodbye)"
|
|
98
|
+
class="suggestions badge badge-solid-error rounded w-14 whitespace-nowrap cursor-pointer"
|
|
99
|
+
@click="send(goodbye)"
|
|
100
|
+
v-html="marked(goodbye)"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div class="divider" />
|
|
105
|
+
|
|
106
|
+
<!-- footer -->
|
|
107
|
+
<footer
|
|
108
|
+
:disabled="!contact.on"
|
|
109
|
+
class="flex item p-3"
|
|
110
|
+
>
|
|
111
|
+
<label class="field-group flex-1 cursor-text mr-2">
|
|
112
|
+
<div
|
|
113
|
+
ref="textareaRef"
|
|
114
|
+
class="textarea input w-48 !max-h-36 overflow-y-auto scrollbar whitespace-pre-wrap cursor-text"
|
|
115
|
+
contenteditable
|
|
116
|
+
@paste.prevent="verifyPaste"
|
|
117
|
+
@keydown.enter="inputSend"
|
|
118
|
+
@keyup="typing(true), persist()"
|
|
119
|
+
@blur="typing(false)"
|
|
120
|
+
/>
|
|
121
|
+
</label>
|
|
122
|
+
<label
|
|
123
|
+
class="h-fit btn"
|
|
124
|
+
@click="reset"
|
|
125
|
+
>
|
|
126
|
+
<i class="bi bi-x-lg text-base text-error" />
|
|
127
|
+
</label>
|
|
128
|
+
<label
|
|
129
|
+
for="chatfiles"
|
|
130
|
+
class="h-fit btn"
|
|
131
|
+
>
|
|
132
|
+
<input
|
|
133
|
+
id="chatfiles"
|
|
134
|
+
type="file"
|
|
135
|
+
class="hidden"
|
|
136
|
+
@change="sendFile"
|
|
137
|
+
>
|
|
138
|
+
<i class="bi bi-paperclip text-xl leading-none" />
|
|
139
|
+
</label>
|
|
140
|
+
|
|
141
|
+
<button
|
|
142
|
+
class="btn-rounded btn btn-solid-primary rounded-full"
|
|
143
|
+
@click="send(getText())"
|
|
144
|
+
>
|
|
145
|
+
<i class="bi-send-fill" />
|
|
146
|
+
</button>
|
|
147
|
+
</footer>
|
|
148
|
+
</div>
|
|
149
|
+
</template>
|
|
150
|
+
|
|
151
|
+
<script setup lang="ts">
|
|
152
|
+
import { ref, computed, watch } from "vue";
|
|
153
|
+
import { IChatContact, IChatMessages, sleep } from "./types";
|
|
154
|
+
import { marked } from "marked";
|
|
155
|
+
import { useNuxtApp } from "#app";
|
|
156
|
+
|
|
157
|
+
// props
|
|
158
|
+
interface IProps {
|
|
159
|
+
contact?: IChatContact;
|
|
160
|
+
messages?: IChatMessages;
|
|
161
|
+
loading?: boolean;
|
|
162
|
+
goodbyes?: string[];
|
|
163
|
+
greetings?: string[];
|
|
164
|
+
absents?: string[];
|
|
165
|
+
persist?: Function;
|
|
166
|
+
defaults?: string;
|
|
167
|
+
subject?: string;
|
|
168
|
+
}
|
|
169
|
+
const props = defineProps<IProps>();
|
|
170
|
+
|
|
171
|
+
// emits
|
|
172
|
+
interface IEmits {
|
|
173
|
+
(e: "back"): void;
|
|
174
|
+
(e: "typing", typing: boolean): void;
|
|
175
|
+
(e: "send", text: string): void;
|
|
176
|
+
(e: "sendFile", file: File): void;
|
|
177
|
+
}
|
|
178
|
+
const emit = defineEmits<IEmits>();
|
|
179
|
+
|
|
180
|
+
// listen
|
|
181
|
+
const { $listen } = useNuxtApp();
|
|
182
|
+
$listen("message:concatenate", (message: any) => {
|
|
183
|
+
if (!textareaRef.value) return;
|
|
184
|
+
|
|
185
|
+
if (textareaRef.value.innerText.trim() === "") {
|
|
186
|
+
textareaRef.value.innerText = message;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
textareaRef.value.innerText = `${textareaRef.value.innerText}
|
|
191
|
+
|
|
192
|
+
${message}`;
|
|
193
|
+
});
|
|
194
|
+
$listen("message:clear", clear)
|
|
195
|
+
|
|
196
|
+
// computed
|
|
197
|
+
const getStatusClass = computed<string>(() => {
|
|
198
|
+
if (props.contact?.on) return "avatar-status-success";
|
|
199
|
+
return "avatar-status-error";
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// data
|
|
203
|
+
const textareaRef = ref<HTMLElement>();
|
|
204
|
+
const OrganismChatMessagesRef = ref<HTMLElement>();
|
|
205
|
+
const lastTyping = ref<boolean>(false);
|
|
206
|
+
|
|
207
|
+
// methods
|
|
208
|
+
async function verifyPaste(): Promise<void> {
|
|
209
|
+
const clipboard = await navigator.clipboard.readText();
|
|
210
|
+
|
|
211
|
+
if (clipboard === "") return;
|
|
212
|
+
set(getText() + clipboard);
|
|
213
|
+
|
|
214
|
+
setEndOfContenteditable(textareaRef.value);
|
|
215
|
+
}
|
|
216
|
+
function persist(): void {
|
|
217
|
+
if (!props.persist) return;
|
|
218
|
+
|
|
219
|
+
props.persist(getText());
|
|
220
|
+
}
|
|
221
|
+
function typing(typing: boolean): void {
|
|
222
|
+
if (lastTyping.value === typing) return;
|
|
223
|
+
|
|
224
|
+
lastTyping.value = typing;
|
|
225
|
+
emit("typing", typing);
|
|
226
|
+
}
|
|
227
|
+
function getText(): string {
|
|
228
|
+
return textareaRef.value?.innerText ?? "";
|
|
229
|
+
}
|
|
230
|
+
async function send(text: string): Promise<void> {
|
|
231
|
+
if (!text.trim().length) return;
|
|
232
|
+
emit("send", text);
|
|
233
|
+
reset();
|
|
234
|
+
await sleep(200);
|
|
235
|
+
typing(false);
|
|
236
|
+
}
|
|
237
|
+
function sendFile({ target }: Event): void {
|
|
238
|
+
const input = target as HTMLInputElement;
|
|
239
|
+
if (!input.files) return;
|
|
240
|
+
const file = input.files[0];
|
|
241
|
+
emit("sendFile", file);
|
|
242
|
+
input.value = "";
|
|
243
|
+
}
|
|
244
|
+
function inputSend(e: KeyboardEvent): void {
|
|
245
|
+
if (e.shiftKey || e.ctrlKey || e.altKey) return;
|
|
246
|
+
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
send(getText());
|
|
249
|
+
}
|
|
250
|
+
function clear(): void {
|
|
251
|
+
if (!textareaRef.value) return;
|
|
252
|
+
textareaRef.value.innerText = "";
|
|
253
|
+
}
|
|
254
|
+
function reset(): void {
|
|
255
|
+
clear()
|
|
256
|
+
persist();
|
|
257
|
+
}
|
|
258
|
+
function set(text: string): void {
|
|
259
|
+
if (textareaRef.value) {
|
|
260
|
+
textareaRef.value.innerText = text;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
set(text);
|
|
266
|
+
}, 1000);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let timer: any;
|
|
270
|
+
function scrollBottom() {
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
timer = setTimeout(() => {
|
|
273
|
+
OrganismChatMessagesRef.value?.scroll({
|
|
274
|
+
top: OrganismChatMessagesRef.value.scrollHeight,
|
|
275
|
+
behavior: "smooth",
|
|
276
|
+
});
|
|
277
|
+
}, 0);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function setEndOfContenteditable(contentEditableElement?: HTMLElement) {
|
|
281
|
+
if (!contentEditableElement) return;
|
|
282
|
+
|
|
283
|
+
const range = document.createRange();
|
|
284
|
+
range.selectNodeContents(contentEditableElement);
|
|
285
|
+
range.collapse(false);
|
|
286
|
+
|
|
287
|
+
const selection = window.getSelection();
|
|
288
|
+
selection?.removeAllRanges();
|
|
289
|
+
selection?.addRange(range);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// watch
|
|
293
|
+
watch(
|
|
294
|
+
() => props.messages,
|
|
295
|
+
() => {
|
|
296
|
+
scrollBottom();
|
|
297
|
+
},
|
|
298
|
+
{ deep: true }
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
watch(
|
|
302
|
+
() => props.defaults,
|
|
303
|
+
() => {
|
|
304
|
+
set(props.defaults ?? "");
|
|
305
|
+
},
|
|
306
|
+
{ deep: true }
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// expose
|
|
310
|
+
defineExpose({ set, reset });
|
|
311
|
+
</script>
|
|
312
|
+
|
|
313
|
+
<style>
|
|
314
|
+
.textarea {
|
|
315
|
+
min-height: 30px;
|
|
316
|
+
border: none !important;
|
|
317
|
+
box-shadow: none !important;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.textarea:focus {
|
|
321
|
+
box-shadow: none !important;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.dark .textarea,
|
|
325
|
+
.dark .textarea:focus {
|
|
326
|
+
border: none !important;
|
|
327
|
+
box-shadow: none !important;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.suggestions > p {
|
|
331
|
+
overflow: hidden;
|
|
332
|
+
text-overflow: ellipsis;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.btn-rounded {
|
|
336
|
+
min-width: 35px;
|
|
337
|
+
max-width: 35px;
|
|
338
|
+
min-height: 35px;
|
|
339
|
+
max-height: 35px;
|
|
340
|
+
padding: 0px !important;
|
|
341
|
+
}
|
|
342
|
+
</style>
|