tantee-nuxt-commons 0.0.169 → 0.0.171

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.
@@ -1,139 +1,160 @@
1
1
  <script lang="ts" setup>
2
- import { VueSignaturePad } from 'vue-signature-pad';
2
+ import { VueSignaturePad } from 'vue-signature-pad'
3
3
  import { VInput } from 'vuetify/components/VInput'
4
- import { type Ref, ref, onMounted, onBeforeUnmount, nextTick,watch,defineExpose,useTemplateRef,computed } from 'vue'
5
-
6
- interface SignatureOptions {
7
- penColor: string
8
- minWidth?: number
9
- maxWidth?: number
10
- }
4
+ import { ref, computed, withDefaults, defineProps, defineExpose, watch } from 'vue'
5
+ import { useAssetFile, type Base64Asset, type Base64File } from '../../composables/assetFile'
11
6
 
12
7
  interface SignatureProps extends /* @vue-ignore */ InstanceType<typeof VInput['$props']> {
13
8
  title?: string
14
9
  btnName?: string
15
10
  titleConfirm?: string
16
- modelValue?: string
11
+ penColor?: string
12
+ /** hydrate when id is present but base64String is missing */
13
+ autoHydrate?: boolean
17
14
  }
18
15
 
19
16
  const props = withDefaults(defineProps<SignatureProps>(), {
20
- title: 'Draw Your Signature',
17
+ title: 'Signature',
21
18
  btnName: 'Draw Your Signature',
22
19
  titleConfirm: 'I Accept My Signature',
20
+ penColor: '#303F9F',
21
+ autoHydrate: true,
23
22
  })
24
23
 
25
- const inputRef = useTemplateRef<VInput>("inputRef")
26
- const signaturePadRef = useTemplateRef<typeof VueSignaturePad>("signaturePadRef")
24
+ const { hydrateAssetFile } = useAssetFile()
27
25
 
28
- const signatureData: Ref<string|null> = ref(null)
29
- const colorOptions: string[] = ['#303F9F', '#1A2023', '#2E7D32', '#AC04BF']
30
- const defaultColor: string = colorOptions[0]
31
- const selectedColor: Ref<string> = ref(defaultColor)
32
- const signatureOptions: Ref<SignatureOptions> = ref({ penColor: defaultColor, minWidth: 0.5, maxWidth: 4 })
33
- const isDialogOpen: Ref<boolean> = ref(false)
26
+ const model = defineModel<Base64Asset | string | null>({ default: null })
34
27
 
35
- const undoSignature = (): void => {
36
- signaturePadRef.value?.undoSignature()
37
- }
28
+ // refs
29
+ const inputRef = ref<InstanceType<typeof VInput> | null>(null)
30
+ const signaturePadRef = ref<InstanceType<typeof VueSignaturePad> | null>(null)
38
31
 
39
- const clearSignature = (): void => {
40
- signaturePadRef.value?.clearSignature()
41
- }
32
+ const isDialogOpen = ref(false)
42
33
 
43
- const closeDialog = (): void => {
44
- isDialogOpen.value = false
45
- signaturePadRef.value?.clearSignature()
46
- signaturePadRef.value?.clearCacheImages()
47
- }
34
+ // signature pad options from props
35
+ const signatureOptions = computed(() => ({
36
+ penColor: props.penColor,
37
+ minWidth: 0.5,
38
+ maxWidth: 4,
39
+ }))
48
40
 
49
- const emit = defineEmits<{
50
- (event: 'update:modelValue', value: string|null): void
51
- }>()
41
+ // internal normalized value
42
+ const normalized = ref<Base64Asset | null>(null)
43
+ const imageDataUrl = computed(() => {
44
+ const val = normalized.value
45
+ if (!val?.base64String) return null
52
46
 
53
- const saveSignature = (): void => {
54
- isDialogOpen.value = false
55
- const { isEmpty, data } = signaturePadRef.value?.saveSignature()
56
- signatureData.value = isEmpty ? null : data
57
- }
47
+ return useAssetFile().ensureDataUrl(val.base64String.trim(),(val as Base64File).fileType || "image/png")
48
+ })
58
49
 
59
- const changePenColor = (color: string): void => {
60
- selectedColor.value = color
61
- signatureOptions.value = { penColor: color }
62
- }
63
50
 
64
- const openSignatureDialog = (): void => {
65
- selectedColor.value = defaultColor
66
- signatureOptions.value = { penColor: defaultColor }
67
- isDialogOpen.value = true
68
- nextTick(() =>{
69
- if (props.modelValue) {
70
- signaturePadRef.value?.fromDataURL(props.modelValue)
71
- }
72
- });
73
- }
51
+ // guards
52
+ let syncing = false // block re-entrancy while normalizing/hydrating
74
53
 
75
- const signaturePadHeight: Ref<string> = ref('')
76
- const updateSignaturePadHeight = () => {
77
- const screenHeight = window.innerHeight
78
- signaturePadHeight.value = `${screenHeight * 0.4}px`
54
+ function wrapToAsset(input: Base64Asset | string | null): Base64Asset | null {
55
+ if (input == null) return null
56
+ return typeof input === 'string' ? { base64String: input } : input
79
57
  }
80
58
 
81
- watch(signatureData,()=>{
82
- emit('update:modelValue', signatureData.value)
83
- })
59
+ /** Normalize & (optionally) hydrate whenever external model changes */
60
+ watch(
61
+ model,
62
+ async (val) => {
63
+ if (syncing) return
64
+ syncing = true
65
+ try {
66
+ const asAsset = wrapToAsset(val)
67
+ normalized.value = asAsset
68
+
69
+ // If parent provided a string, convert and write back once
70
+ if (typeof val === 'string') {
71
+ model.value = asAsset
72
+ }
73
+
74
+ // Hydrate if requested and needed (id present, no base64String)
75
+ if (props.autoHydrate && asAsset?.id != null && !asAsset.base64String) {
76
+ await hydrateAssetFile(asAsset) // mutates asAsset in-place
77
+ // reflect hydrated base64String back to parent (guarded)
78
+ model.value = asAsset
79
+ }
80
+ } finally {
81
+ syncing = false
82
+ }
83
+ },
84
+ { immediate: true }
85
+ )
86
+
87
+ // signature actions
88
+ const undoSignature = () => signaturePadRef.value?.undoSignature()
89
+ const clearSignature = () => signaturePadRef.value?.clearSignature()
90
+
91
+ const closeDialog = () => {
92
+ isDialogOpen.value = false
93
+ signaturePadRef.value?.clearSignature()
94
+ signaturePadRef.value?.clearCacheImages?.()
95
+ }
84
96
 
85
- watch(()=>props.modelValue, (newValue) => {
86
- signatureData.value = newValue || null
87
- },{immediate: true})
97
+ const saveSignature = () => {
98
+ isDialogOpen.value = false
99
+ const result = signaturePadRef.value?.saveSignature()
100
+ if (!result) return
101
+ const { isEmpty, data } = result
102
+ if (isEmpty) {
103
+ normalized.value = null
104
+ model.value = null
105
+ } else {
106
+ const asset: Base64Asset = { base64String: data }
107
+ normalized.value = asset
108
+ model.value = asset
109
+ }
110
+ }
88
111
 
89
- onMounted(() => {
90
- updateSignaturePadHeight()
91
- window.addEventListener('resize', updateSignaturePadHeight)
92
- })
112
+ const openSignatureDialog = async () => {
113
+ // ensure hydration before opening to preview correctly
114
+ if (props.autoHydrate && normalized.value?.id != null && !normalized.value.base64String) {
115
+ await hydrateAssetFile(normalized.value)
116
+ model.value = normalized.value
117
+ }
93
118
 
94
- onBeforeUnmount(() => {
95
- window.removeEventListener('resize', updateSignaturePadHeight)
96
- })
119
+ isDialogOpen.value = true
97
120
 
98
- const isValid = computed(()=>{
99
- return inputRef.value?.isValid
100
- })
121
+ const existing = normalized.value?.base64String
122
+ if (existing) {
123
+ // seed after dialog mount
124
+ requestAnimationFrame(() => signaturePadRef.value?.fromDataURL(existing))
125
+ }
126
+ }
101
127
 
102
- const errorMessages = computed(()=>{
103
- return inputRef.value?.errorMessages
104
- })
128
+ // validation passthrough
129
+ const isValid = computed(() => inputRef.value?.isValid)
130
+ const errorMessages = computed(() => inputRef.value?.errorMessages)
105
131
 
106
132
  defineExpose({
107
133
  errorMessages,
108
134
  isValid,
109
- reset: ()=>inputRef.value?.reset(),
110
- resetValidation : ()=>inputRef.value?.resetValidation(),
111
- validate : ()=>inputRef.value?.validate(),
135
+ reset: () => inputRef.value?.reset(),
136
+ resetValidation: () => inputRef.value?.resetValidation(),
137
+ validate: () => inputRef.value?.validate(),
112
138
  })
113
139
  </script>
114
140
 
115
141
  <template>
116
- <v-input v-model="signatureData" v-bind="$attrs" ref="inputRef">
117
- <template #default="{isReadonly,isDisabled}">
118
- <v-card
119
- class="w-100"
120
- flat
121
- >
122
- <v-card-text v-if="signatureData">
123
- <v-img
124
- :src="signatureData"
125
- cover
126
- />
142
+ <v-input v-model="model" v-bind="$attrs" ref="inputRef">
143
+ <template #default="{ isReadonly, isDisabled }">
144
+ <v-card class="w-100" flat :variant="$attrs.variant" :title="props.title">
145
+ <v-card-text v-if="normalized?.base64String">
146
+ <v-img :src="imageDataUrl" cover />
127
147
  <v-icon
128
148
  class="position-absolute"
129
149
  style="top: 8px; right: 8px; z-index: 10;"
130
- @click="signatureData=null"
131
- v-if="signatureData && !isReadonly.value"
150
+ @click="model = null; normalized = null"
151
+ v-if="!isReadonly?.value"
132
152
  >
133
153
  mdi mdi-close-circle
134
154
  </v-icon>
135
155
  </v-card-text>
136
- <v-card-actions>
156
+
157
+ <v-card-actions v-if="!isReadonly?.value">
137
158
  <v-btn
138
159
  append-icon="mdi mdi-draw-pen"
139
160
  block
@@ -141,79 +162,43 @@ defineExpose({
141
162
  color="primary"
142
163
  variant="flat"
143
164
  @click="openSignatureDialog"
144
- :readonly="isReadonly.value"
145
- :disabled="isDisabled.value"
165
+ :readonly="isReadonly?.value"
166
+ :disabled="isDisabled?.value"
146
167
  >
147
168
  {{ props.btnName }}
148
169
  </v-btn>
149
170
  </v-card-actions>
150
- <v-dialog
151
- v-model="isDialogOpen"
152
- height="auto"
153
- persistent
154
- width="100%"
155
- >
171
+
172
+ <v-dialog v-model="isDialogOpen" height="auto" persistent width="100%">
156
173
  <v-card>
157
174
  <v-toolbar>
158
175
  <v-toolbar-title class="text-no-wrap">
159
176
  {{ props.title }}
160
177
  </v-toolbar-title>
161
- <v-btn
162
- icon
163
- @click="undoSignature"
164
- >
178
+
179
+ <v-btn icon @click="undoSignature">
165
180
  <v-icon>fa-solid fa-arrow-rotate-left</v-icon>
166
181
  </v-btn>
167
- <v-btn
168
- icon
169
- @click="clearSignature"
170
- >
182
+
183
+ <v-btn icon @click="clearSignature">
171
184
  <v-icon>fa-solid fa-trash</v-icon>
172
185
  </v-btn>
173
- <v-menu>
174
- <template #activator="{ props: activatorProps }">
175
- <v-btn
176
- :color="selectedColor"
177
- :icon="true"
178
- v-bind="activatorProps"
179
- >
180
- <v-icon>fa-solid fa-paintbrush</v-icon>
181
- </v-btn>
182
- </template>
183
- <v-list>
184
- <v-row>
185
- <v-col class="text-center">
186
- <v-avatar
187
- v-for="(color, index) in colorOptions"
188
- :key="index"
189
- :color="color"
190
- :value="color"
191
- class="mr-1"
192
- @click="changePenColor(color)"
193
- >
194
- <v-icon color="white">
195
- {{ selectedColor === color ? 'fa-solid fa-check' : '' }}
196
- </v-icon>
197
- </v-avatar>
198
- </v-col>
199
- </v-row>
200
- </v-list>
201
- </v-menu>
202
- <v-btn
203
- icon
204
- @click="closeDialog"
205
- >
186
+
187
+ <v-btn icon @click="closeDialog">
206
188
  <v-icon>fa-solid fa-xmark</v-icon>
207
189
  </v-btn>
208
190
  </v-toolbar>
191
+
209
192
  <v-card-text>
210
193
  <VueSignaturePad
211
194
  ref="signaturePadRef"
212
195
  :options="signatureOptions"
213
- :height="signaturePadHeight"
196
+ height="40vh"
214
197
  />
215
198
  </v-card-text>
199
+
216
200
  <v-divider />
201
+
217
202
  <v-card-actions class="justify-center">
218
203
  <v-btn
219
204
  class="text-none"