@ramathibodi/nuxt-commons 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +81 -0
  2. package/dist/module.cjs +5 -0
  3. package/dist/module.d.mts +7 -0
  4. package/dist/module.d.ts +7 -0
  5. package/dist/module.json +8 -0
  6. package/dist/module.mjs +34 -0
  7. package/dist/runtime/components/Alert.vue +52 -0
  8. package/dist/runtime/components/BarcodeReader.vue +98 -0
  9. package/dist/runtime/components/Calendar.vue +99 -0
  10. package/dist/runtime/components/Camera.vue +116 -0
  11. package/dist/runtime/components/ExportCSV.vue +55 -0
  12. package/dist/runtime/components/FileBtn.vue +56 -0
  13. package/dist/runtime/components/ImportCSV.vue +64 -0
  14. package/dist/runtime/components/Pdf/Print.vue +63 -0
  15. package/dist/runtime/components/Pdf/View.vue +70 -0
  16. package/dist/runtime/components/TabsGroup.vue +28 -0
  17. package/dist/runtime/components/TextBarcode.vue +52 -0
  18. package/dist/runtime/components/dialog/Confirm.vue +100 -0
  19. package/dist/runtime/components/dialog/Index.vue +72 -0
  20. package/dist/runtime/components/dialog/Loading.vue +34 -0
  21. package/dist/runtime/components/form/Date.vue +163 -0
  22. package/dist/runtime/components/form/DateTime.vue +107 -0
  23. package/dist/runtime/components/form/File.vue +187 -0
  24. package/dist/runtime/components/form/Login.vue +131 -0
  25. package/dist/runtime/components/form/Pad.vue +179 -0
  26. package/dist/runtime/components/form/SignPad.vue +186 -0
  27. package/dist/runtime/components/form/Time.vue +158 -0
  28. package/dist/runtime/components/form/images/CameraCrop.vue +58 -0
  29. package/dist/runtime/components/form/images/Edit.vue +143 -0
  30. package/dist/runtime/components/form/images/Preview.vue +48 -0
  31. package/dist/runtime/components/label/Date.vue +29 -0
  32. package/dist/runtime/components/label/FormatMoney.vue +29 -0
  33. package/dist/runtime/composables/alert.d.ts +13 -0
  34. package/dist/runtime/composables/alert.mjs +44 -0
  35. package/dist/runtime/composables/utils/validation.d.ts +32 -0
  36. package/dist/runtime/composables/utils/validation.mjs +36 -0
  37. package/dist/runtime/labs/form/EditMobile.vue +153 -0
  38. package/dist/runtime/labs/form/TextFieldMask.vue +43 -0
  39. package/dist/runtime/plugins/vueSignaturePad.d.ts +2 -0
  40. package/dist/runtime/plugins/vueSignaturePad.mjs +5 -0
  41. package/dist/runtime/types/alert.d.ts +11 -0
  42. package/dist/runtime/types/modules.d.ts +5 -0
  43. package/dist/runtime/utils/datetime.d.ts +25 -0
  44. package/dist/runtime/utils/datetime.mjs +166 -0
  45. package/dist/runtime/utils/object.d.ts +8 -0
  46. package/dist/runtime/utils/object.mjs +28 -0
  47. package/dist/types.d.mts +16 -0
  48. package/dist/types.d.ts +16 -0
  49. package/package.json +90 -0
@@ -0,0 +1,187 @@
1
+ <script lang="ts" setup>
2
+ import { uniqWith, isEqual } from 'lodash-es'
3
+ import { ref, watch } from 'vue'
4
+ import { VTextField } from 'vuetify/components/VTextField'
5
+ import { useAlert } from '../../composables/alert'
6
+
7
+ const alert = useAlert()
8
+
9
+ interface Base64String {
10
+ base64String?: string
11
+ fileName: string
12
+ id?: number
13
+ }
14
+
15
+ interface Props extends /* @vue-ignore */ InstanceType<typeof VTextField['$props']> {
16
+ accept?: string
17
+ multiple?: boolean
18
+ maxSize?: number
19
+ modelValue?: Base64String[]
20
+ }
21
+
22
+ const props = withDefaults(defineProps<Props>(), {
23
+ accept: '*',
24
+ multiple: false,
25
+ maxSize: 5,
26
+ })
27
+
28
+ const emit = defineEmits<{
29
+ (e: 'update:modelValue', value: Base64String[]): void
30
+ }>()
31
+
32
+ const allFiles = ref<File[]>([])
33
+ const allAssets = ref<Base64String[]>([])
34
+ const combinedBase64String = ref<Base64String[]>([])
35
+ const fileInput = ref()
36
+
37
+ function openWindowUpload() {
38
+ if (props.multiple || (!allFiles.value?.length && !allAssets.value?.length)) {
39
+ fileInput.value?.click()
40
+ }
41
+ }
42
+
43
+ function addFiles(files: File | File[]) {
44
+ if (Array.isArray(files)) allFiles.value?.push(...files)
45
+ else allFiles.value?.push(files)
46
+ }
47
+
48
+ function removeFileByIndex(i: number | string) {
49
+ const index = Number(i)
50
+ if (Array.isArray(allFiles.value) && allFiles.value.length) {
51
+ if (index >= 0 && index < allFiles.value.length) allFiles.value.splice(index, 1)
52
+ }
53
+ }
54
+
55
+ function removeAssetByIndex(i: number | string) {
56
+ const index = Number(i)
57
+ if (Array.isArray(allAssets.value) && allAssets.value.length) {
58
+ if (index >= 0 && index < allAssets.value.length) allAssets.value.splice(index, 1)
59
+ }
60
+ }
61
+
62
+ function fileToBase64(file: File) {
63
+ const maxSize = props.maxSize * 1048576
64
+
65
+ return new Promise<Base64String>((resolve, reject) => {
66
+ if (file.size > maxSize) reject (`File (${file.name}) size exceeds the ${props.maxSize} MB limit.`)
67
+
68
+ const reader = new FileReader()
69
+ reader.onload = function (event) {
70
+ resolve({ fileName: file.name, base64String: event.target?.result as string })
71
+ }
72
+ reader.onerror = function (error) {
73
+ reject(error)
74
+ }
75
+ reader.readAsDataURL(file)
76
+ })
77
+ }
78
+
79
+ function base64ToFile(base64Data: string, filename: string) {
80
+ // Extract content type and base64 payload from the Base64 string
81
+ const matchResult = base64Data.match(/data:([^;]*);base64,(.*)/)
82
+ if (matchResult === null) {
83
+ return undefined
84
+ }
85
+ const [contentType, base64Payload] = matchResult.slice(1)
86
+
87
+ // Convert base64 to a Uint8Array
88
+ const binaryStr = atob(base64Payload)
89
+ const len = binaryStr.length
90
+ const bytes = new Uint8Array(len)
91
+ for (let i = 0; i < len; i++) {
92
+ bytes[i] = binaryStr.charCodeAt(i)
93
+ }
94
+
95
+ return new File([bytes], filename, { type: contentType })
96
+ }
97
+
98
+ watch(
99
+ () => props.modelValue,
100
+ () => {
101
+ if (props.modelValue && Array.isArray(props.modelValue)) {
102
+ allAssets.value = props.modelValue.filter((item: Base64String) => item.id !== undefined)
103
+ allFiles.value = props.modelValue.filter((item: Base64String) => item.id === undefined && item.base64String !== undefined).map((base64: Base64String) => base64ToFile(base64.base64String as string, base64.fileName)).filter((item: File | undefined) => item !== undefined) as File[]
104
+ }
105
+ else {
106
+ allAssets.value = []
107
+ allFiles.value = []
108
+ }
109
+ },
110
+ { deep: true, immediate: true },
111
+ )
112
+
113
+ watch([allAssets, allFiles], () => {
114
+ if (allFiles.value && allFiles.value?.length) {
115
+ const base64Promises = allFiles.value?.map(file => fileToBase64(file))
116
+
117
+ Promise.all(base64Promises).then((base64Strings) => {
118
+ combinedBase64String.value = [...allAssets.value, ...base64Strings]
119
+ }).catch((error) => {
120
+ alert?.addAlert({ message: error, alertType: 'error' })
121
+ allFiles.value = []
122
+ })
123
+ }
124
+ else {
125
+ combinedBase64String.value = [...allAssets.value]
126
+ }
127
+ }, { deep: true, immediate: true })
128
+
129
+ watch(combinedBase64String, (newValue, oldValue) => {
130
+ if (!isEqual(newValue, oldValue)) {
131
+ emit('update:modelValue', uniqWith(newValue, isEqual))
132
+ }
133
+ }, { deep: true })
134
+ </script>
135
+
136
+ <template>
137
+ <v-text-field
138
+ v-bind="$attrs"
139
+ label="Upload files"
140
+ readonly
141
+ :dirty="combinedBase64String.length>0"
142
+ v-on="(combinedBase64String.length>0) ? {} : { click: openWindowUpload }"
143
+ >
144
+ <template #default>
145
+ <v-chip
146
+ v-for="(asset, index) in allAssets"
147
+ :key="asset"
148
+ color="green"
149
+ variant="flat"
150
+ closable
151
+ @click:close="removeAssetByIndex(index)"
152
+ >
153
+ {{ asset.fileName }}
154
+ </v-chip>
155
+ <v-chip
156
+ v-for="(file, index) in allFiles"
157
+ :key="file"
158
+ color="primary"
159
+ variant="flat"
160
+ closable
161
+ @click:close="removeFileByIndex(index)"
162
+ >
163
+ {{ file.name }}
164
+ </v-chip>
165
+ </template>
166
+
167
+ <template
168
+ v-if="combinedBase64String.length>0 && props.multiple"
169
+ #append-inner
170
+ >
171
+ <VBtn
172
+ variant="text"
173
+ :icon="true"
174
+ @click="openWindowUpload"
175
+ >
176
+ <v-icon>mdi mdi-plus</v-icon>
177
+ </VBtn>
178
+ </template>
179
+ </v-text-field>
180
+ <v-file-input
181
+ ref="fileInput"
182
+ :accept="props.accept"
183
+ :multiple="props.multiple"
184
+ style="display: none"
185
+ @update:modelValue="addFiles"
186
+ />
187
+ </template>
@@ -0,0 +1,131 @@
1
+ <script lang="ts" setup>
2
+ import { ref, watch, withDefaults } from 'vue'
3
+
4
+ interface Props {
5
+ title?: string
6
+ btnSubmit?: string
7
+ imgLogo?: string
8
+ errorMessages?: string
9
+ loading?: boolean
10
+ hint?: string
11
+ }
12
+
13
+ const props = withDefaults(defineProps<Props>(), {
14
+ title: 'Login Ramathibodi',
15
+ btnSubmit: 'เข้าสู่ระบบ',
16
+ imgLogo: '',
17
+ errorMessages: '',
18
+ loading: false,
19
+ hint: '',
20
+ })
21
+
22
+ interface LoginData {
23
+ username: string
24
+ password: string
25
+ }
26
+
27
+ const formValid = ref<boolean>(false)
28
+ const loginData = ref<LoginData>({ username: '', password: '' })
29
+ const emit = defineEmits(['update:modelValue'])
30
+
31
+ const errorText = ref<string>('')
32
+
33
+ const clearErrorMessages = () => {
34
+ errorText.value = ''
35
+ }
36
+
37
+ watch(() => props.errorMessages, (newVal) => {
38
+ if (errorText.value !== newVal) {
39
+ errorText.value = newVal
40
+ }
41
+ })
42
+
43
+ const onSubmit = () => {
44
+ if (!formValid.value) return
45
+ emit('update:modelValue', { ...loginData.value })
46
+ }
47
+ </script>
48
+
49
+ <template>
50
+ <v-row
51
+ justify="center"
52
+ align="center"
53
+ >
54
+ <v-col
55
+ cols="12"
56
+ xs="12"
57
+ sm="8"
58
+ md="6"
59
+ lg="6"
60
+ xl="4"
61
+ align-self="center"
62
+ >
63
+ <v-sheet
64
+ class="pa-3"
65
+ rounded
66
+ >
67
+ <v-card class="mx-auto px-6 py-6">
68
+ <v-img
69
+ :src="props.imgLogo"
70
+ cover
71
+ />
72
+ <v-card-title class="text-primary text-center">
73
+ {{ props.title }}
74
+ </v-card-title>
75
+ <v-form
76
+ v-model="formValid"
77
+ @submit.prevent="onSubmit"
78
+ >
79
+ <form-pad
80
+ ref="formPadTemplate"
81
+ v-model="loginData"
82
+ >
83
+ <template #default="{ data, rules }">
84
+ <v-text-field
85
+ ref="usernameField"
86
+ v-model="data.username"
87
+ color="primary"
88
+ label="ชื่อผู้ใช้งาน (Username)"
89
+ clearable
90
+ :rules="[rules.require('กรุณาระบุ')]"
91
+ autofocus
92
+ @update:model-value="clearErrorMessages()"
93
+ />
94
+ <v-text-field
95
+ ref="passwordField"
96
+ v-model="data.password"
97
+ color="primary"
98
+ label="รหัสผ่าน (Password)"
99
+ type="password"
100
+ clearable
101
+ :rules="[rules.require('กรุณาระบุ')]"
102
+ autofocus
103
+ @update:model-value="clearErrorMessages()"
104
+ />
105
+ </template>
106
+ </form-pad>
107
+ <div class="text-body-2 text-error">
108
+ {{ errorText }}
109
+ </div>
110
+ <div class="my-3">
111
+ <slot name="hint">
112
+ {{ props.hint }}
113
+ </slot>
114
+ </div>
115
+ <v-btn
116
+ :disabled="!formValid"
117
+ :loading="props.loading"
118
+ block
119
+ color="primary"
120
+ size="large"
121
+ type="submit"
122
+ variant="elevated"
123
+ >
124
+ {{ props.btnSubmit }}
125
+ </v-btn>
126
+ </v-form>
127
+ </v-card>
128
+ </v-sheet>
129
+ </v-col>
130
+ </v-row>
131
+ </template>
@@ -0,0 +1,179 @@
1
+ <script lang="ts" setup>
2
+ /* eslint-disable @typescript-eslint/no-explicit-any,import/no-self-import */
3
+ import { compile, defineComponent, inject, onMounted, ref, shallowRef, watch, computed, withDefaults } from 'vue'
4
+ import { isObject } from 'lodash-es'
5
+ import { watchDebounced } from '@vueuse/core'
6
+ import { useRules } from '../../composables/utils/validation'
7
+ import FormPad from './Pad.vue'
8
+
9
+ interface Props {
10
+ modelValue?: object
11
+ template?: any
12
+ templateScript?: string
13
+ disabled?: boolean
14
+ readonly?: boolean
15
+ }
16
+
17
+ const props = withDefaults(defineProps<Props>(), {
18
+ disabled: false,
19
+ readonly: false,
20
+ })
21
+
22
+ const emit = defineEmits(['update:modelValue'])
23
+
24
+ const disabled = ref(props.disabled)
25
+ const readonly = ref(props.readonly)
26
+
27
+ watch(() => props.disabled, (newValue) => {
28
+ disabled.value = newValue
29
+ })
30
+
31
+ watch(() => props.readonly, (newValue) => {
32
+ readonly.value = newValue
33
+ })
34
+
35
+ const { rules } = useRules()
36
+
37
+ const trimmedTemplate = computed(() => props.template?.trim() || '')
38
+
39
+ const templateScriptFunction = computed(() => {
40
+ let templateScript = props.templateScript?.trim() || 'return {}'
41
+ const pattern = /^\s*[{[].*[}\]]\s*$/
42
+ if (pattern.test(templateScript)) templateScript = 'return {}'
43
+ return Function('props', 'ctx', templateScript)
44
+ })
45
+
46
+ const formPad = ref()
47
+ const formInjectKey = Symbol.for('vuetify:form')
48
+ const formInjected = ref()
49
+
50
+ const formData = ref<any>({})
51
+
52
+ watch(formData, (newValue) => {
53
+ emit('update:modelValue', newValue)
54
+ }, { deep: true })
55
+
56
+ watch(() => props.modelValue, (newValue) => {
57
+ formData.value = isObject(newValue) ? newValue : {}
58
+ }, { deep: true, immediate: true })
59
+
60
+ const formComponent = shallowRef()
61
+
62
+ function buildFormComponent() {
63
+ if (!trimmedTemplate.value) return
64
+ const originalConsoleError = console.warn
65
+ console.warn = (error) => { throw new Error(error) } // eslint-disable-line
66
+ try {
67
+ const componentTemplate = '<form-pad ref="formPadTemplate" v-model="formComponentData" :disabled="disabled" :readonly="readonly"><template v-slot="{ data,isDisabled,isReadonly,rules,formProvided }">' + trimmedTemplate.value + '</template></form-pad>'
68
+ compile(componentTemplate)
69
+ formComponent.value = defineComponent({
70
+ components: { FormPad },
71
+ props: {
72
+ modelValue: { type: Object, default: undefined },
73
+ disabled: { type: Boolean, default: false },
74
+ readonly: { type: Boolean, default: false },
75
+ },
76
+ emits: ['update:modelValue'],
77
+ setup(props, ctx) {
78
+ const formComponentData = ref<any>({})
79
+ const formPadTemplate = ref<any>({})
80
+ watch(formComponentData, (newValue) => {
81
+ ctx.emit('update:modelValue', newValue)
82
+ }, { deep: true })
83
+ watch(() => props.modelValue, (newValue) => {
84
+ formComponentData.value = isObject(newValue) ? newValue : {}
85
+ }, { deep: true, immediate: true })
86
+ const isValid = computed(() => formPadTemplate.value.isValid)
87
+ return {
88
+ formComponentData,
89
+ formPadTemplate,
90
+ reset: () => formPadTemplate.value.reset(),
91
+ validate: () => formPadTemplate.value.validate(),
92
+ resetValidate: () => formPadTemplate.value.resetValidate(),
93
+ isValid,
94
+ ...templateScriptFunction.value(props, ctx),
95
+ }
96
+ },
97
+ template: componentTemplate,
98
+ })
99
+ }
100
+ catch (e) {
101
+ formComponent.value = null
102
+ console.error(e)
103
+ }
104
+ console.warn = originalConsoleError
105
+ }
106
+
107
+ function reset() {
108
+ if (!formInjected.value) formPad.value.reset()
109
+ else formInjected.value.reset()
110
+ }
111
+
112
+ function validate() {
113
+ if (!formInjected.value) formPad.value.validate()
114
+ else formInjected.value.validate()
115
+ }
116
+
117
+ function resetValidate() {
118
+ if (!formInjected.value) formPad.value.resetValidate()
119
+ else formInjected.value.resetValidate()
120
+ }
121
+
122
+ const isValid = computed(() => {
123
+ validate()
124
+ return formInjected.value ? formInjected.value.isValid || false : formPad.value.isValid || false
125
+ })
126
+
127
+ onMounted(() => {
128
+ formInjected.value = inject(formInjectKey, false)
129
+ buildFormComponent()
130
+ })
131
+
132
+ watchDebounced(() => props.template, buildFormComponent, { debounce: 1000, maxWait: 5000 })
133
+ watchDebounced(() => props.templateScript, buildFormComponent, { debounce: 1000, maxWait: 5000 })
134
+
135
+ defineExpose({
136
+ isValid,
137
+ disabled,
138
+ readonly,
139
+ reset,
140
+ validate,
141
+ resetValidate,
142
+ })
143
+ </script>
144
+
145
+ <template>
146
+ <v-form
147
+ v-if="!formInjected && !trimmedTemplate"
148
+ ref="formPad"
149
+ :disabled="disabled"
150
+ :readonly="readonly"
151
+ >
152
+ <template #default="formProvided">
153
+ <slot
154
+ :data="formData"
155
+ :form-provided="formProvided"
156
+ :is-disabled="disabled"
157
+ :is-readonly="readonly"
158
+ :rules="rules"
159
+ />
160
+ </template>
161
+ </v-form>
162
+ <template v-if="formInjected && !trimmedTemplate">
163
+ <slot
164
+ :data="formData"
165
+ :form-provided="formInjected"
166
+ :is-disabled="disabled"
167
+ :is-readonly="readonly"
168
+ :rules="rules"
169
+ />
170
+ </template>
171
+ <component
172
+ :is="formComponent"
173
+ v-if="trimmedTemplate"
174
+ ref="formPad"
175
+ v-model="formData"
176
+ :disabled="disabled"
177
+ :readonly="readonly"
178
+ />
179
+ </template>
@@ -0,0 +1,186 @@
1
+ <script lang="ts" setup>
2
+ import { VueSignaturePad } from 'vue-signature-pad'
3
+ import { type Ref, ref, onMounted, onBeforeUnmount, withDefaults } from 'vue'
4
+
5
+ interface SignatureOptions {
6
+ penColor: string
7
+ minWidth?: number
8
+ maxWidth?: number
9
+ }
10
+
11
+ interface SignatureProps {
12
+ title?: string
13
+ titleConfirm?: string
14
+ modelValue: string
15
+ }
16
+
17
+ const props = withDefaults(defineProps<SignatureProps>(), {
18
+ title: 'Draw Your Signature',
19
+ titleConfirm: 'I Accept My Signature',
20
+ })
21
+
22
+ const signaturePadRef: Ref<any> = ref(null)
23
+ const signatureData: Ref<string> = ref('')
24
+ const colorOptions: string[] = ['#303F9F', '#1A2023', '#2E7D32', '#AC04BF']
25
+ const defaultColor: string = colorOptions[0]
26
+ const selectedColor: Ref<string> = ref(defaultColor)
27
+ const signatureOptions: Ref<SignatureOptions> = ref({ penColor: defaultColor, minWidth: 0.5, maxWidth: 4 })
28
+ const isDialogOpen: Ref<boolean> = ref(false)
29
+
30
+ const undoSignature = (): void => {
31
+ signaturePadRef.value.undoSignature()
32
+ }
33
+
34
+ const clearSignature = (): void => {
35
+ signaturePadRef.value.clearSignature()
36
+ }
37
+
38
+ const closeDialog = (): void => {
39
+ isDialogOpen.value = false
40
+ signaturePadRef.value.clearSignature()
41
+ signaturePadRef.value.clearCacheImages()
42
+ }
43
+
44
+ const emit = defineEmits<{
45
+ (event: 'update:modelValue', value: string): void
46
+ }>()
47
+
48
+ const saveSignature = (): void => {
49
+ isDialogOpen.value = false
50
+ const { isEmpty, data } = signaturePadRef.value.saveSignature()
51
+ signatureData.value = isEmpty ? '' : data
52
+ emit('update:modelValue', signatureData.value)
53
+ }
54
+
55
+ const changePenColor = (color: string): void => {
56
+ selectedColor.value = color
57
+ signatureOptions.value = { penColor: color }
58
+ }
59
+
60
+ const openSignatureDialog = (): void => {
61
+ selectedColor.value = defaultColor
62
+ signatureOptions.value = { penColor: defaultColor }
63
+ isDialogOpen.value = true
64
+ }
65
+
66
+ const signaturePadHeight: Ref<string> = ref('')
67
+ const updateSignaturePadHeight = () => {
68
+ const screenHeight = window.innerHeight
69
+ signaturePadHeight.value = `${screenHeight * 0.4}px`
70
+ }
71
+
72
+ onMounted(() => {
73
+ updateSignaturePadHeight()
74
+ window.addEventListener('resize', updateSignaturePadHeight)
75
+ })
76
+
77
+ onBeforeUnmount(() => {
78
+ window.removeEventListener('resize', updateSignaturePadHeight)
79
+ })
80
+ </script>
81
+
82
+ <template>
83
+ <v-card
84
+ v-if="signatureData"
85
+ class="pa-2 mb-1"
86
+ color="grey-lighten-1"
87
+ variant="outlined"
88
+ @click="openSignatureDialog"
89
+ >
90
+ <v-img
91
+ :src="signatureData"
92
+ cover
93
+ />
94
+ </v-card>
95
+ <v-btn
96
+ append-icon="mdi mdi-draw-pen"
97
+ block
98
+ class="text-none"
99
+ color="primary"
100
+ variant="flat"
101
+ @click="openSignatureDialog"
102
+ >
103
+ {{ props.title }}
104
+ </v-btn>
105
+
106
+ <v-dialog
107
+ v-model="isDialogOpen"
108
+ height="auto"
109
+ persistent
110
+ width="100%"
111
+ >
112
+ <v-card>
113
+ <v-toolbar>
114
+ <v-toolbar-title class="text-no-wrap">
115
+ {{ props.title }}
116
+ </v-toolbar-title>
117
+ <v-btn
118
+ icon
119
+ @click="undoSignature"
120
+ >
121
+ <v-icon>fa-solid fa-arrow-rotate-left</v-icon>
122
+ </v-btn>
123
+ <v-btn
124
+ icon
125
+ @click="clearSignature"
126
+ >
127
+ <v-icon>fa-solid fa-trash</v-icon>
128
+ </v-btn>
129
+ <v-menu>
130
+ <template #activator="{ props : activatorProps }">
131
+ <v-btn
132
+ :color="selectedColor"
133
+ :icon="true"
134
+ v-bind="activatorProps"
135
+ >
136
+ <v-icon>fa-solid fa-paintbrush</v-icon>
137
+ </v-btn>
138
+ </template>
139
+ <v-list>
140
+ <v-row>
141
+ <v-col class="text-center">
142
+ <v-avatar
143
+ v-for="(color, index) in colorOptions"
144
+ :color="color"
145
+ :value="color"
146
+ class="mr-1"
147
+ @click="changePenColor(color)"
148
+ :key="index"
149
+ >
150
+ <v-icon color="white">
151
+ {{ selectedColor === color ? 'fa-solid fa-check' : '' }}
152
+ </v-icon>
153
+ </v-avatar>
154
+ </v-col>
155
+ </v-row>
156
+ </v-list>
157
+ </v-menu>
158
+ <v-btn
159
+ icon
160
+ @click="closeDialog"
161
+ >
162
+ <v-icon>fa-solid fa-xmark</v-icon>
163
+ </v-btn>
164
+ </v-toolbar>
165
+ <v-card-text>
166
+ <VueSignaturePad
167
+ ref="signaturePadRef"
168
+ :options="signatureOptions"
169
+ :height="signaturePadHeight"
170
+ />
171
+ </v-card-text>
172
+ <v-divider />
173
+ <v-card-actions class="justify-center">
174
+ <v-btn
175
+ class="text-none"
176
+ color="success"
177
+ prepend-icon="fa-solid fa-check"
178
+ variant="flat"
179
+ @click="saveSignature"
180
+ >
181
+ {{ props.titleConfirm }}
182
+ </v-btn>
183
+ </v-card-actions>
184
+ </v-card>
185
+ </v-dialog>
186
+ </template>