tantee-nuxt-commons 0.0.167 → 0.0.169
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/Alert.vue +1 -0
- package/dist/runtime/components/form/ActionPad.vue +1 -1
- package/dist/runtime/components/form/Dialog.vue +1 -1
- package/dist/runtime/components/form/EditPad.vue +1 -1
- package/dist/runtime/components/form/File.vue +126 -189
- package/dist/runtime/components/form/Iterator.vue +0 -2
- package/dist/runtime/components/form/Pad.vue +71 -2
- package/dist/runtime/components/form/SignPad.vue +139 -131
- package/dist/runtime/components/form/images/Field.vue +165 -185
- package/dist/runtime/components/label/DateCount.vue +117 -13
- package/dist/runtime/components/pdf/View.vue +42 -32
- package/dist/runtime/composables/document/templateFormTable.js +4 -1
- package/package.json +1 -1
- package/dist/runtime/composables/assetFile.d.ts +0 -31
- package/dist/runtime/composables/assetFile.js +0 -134
package/dist/module.json
CHANGED
|
@@ -45,6 +45,7 @@ watch(() => alert?.hasAlert(), (hasAlert) => {
|
|
|
45
45
|
elevation="2"
|
|
46
46
|
theme="dark"
|
|
47
47
|
variant="flat"
|
|
48
|
+
v-bind="currentItem.alertIcon ? { icon: currentItem.alertIcon } : {}"
|
|
48
49
|
@click:close="renewAlert()"
|
|
49
50
|
>
|
|
50
51
|
{{ currentItem.statusCode ? currentItem.statusCode + ' ' : '' }} {{ currentItem.message }} {{ !isEmpty(currentItem.data) ? currentItem.data : '' }}
|
|
@@ -85,7 +85,7 @@ const loadFormData = () => {
|
|
|
85
85
|
formDataOriginalValue.value = cloneDeep(props.formData)
|
|
86
86
|
}
|
|
87
87
|
else {
|
|
88
|
-
formData.value = Object.assign({}, props.initialData)
|
|
88
|
+
formData.value = Object.assign({}, cloneDeep(props.initialData))
|
|
89
89
|
}
|
|
90
90
|
isSavedAndStay.value = false
|
|
91
91
|
}
|
|
@@ -1,225 +1,172 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { type Base64File } from '../../composables/assetFile'
|
|
7
|
-
import { useAssetFile } from '../../composables/assetFile'
|
|
2
|
+
import {isEqual, uniqWith} from 'lodash-es'
|
|
3
|
+
import {ref, watch} from 'vue'
|
|
4
|
+
import {VTextField} from 'vuetify/components/VTextField'
|
|
5
|
+
import {useAlert} from '../../composables/alert'
|
|
8
6
|
|
|
9
7
|
const alert = useAlert()
|
|
10
|
-
const { hydrateAssetFile, base64ToFile, fileToBase64, downloadBase64File } = useAssetFile()
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
interface Base64String {
|
|
10
|
+
base64String?: string
|
|
11
|
+
fileName: string
|
|
12
|
+
originalFileName?: string
|
|
13
|
+
id?: number
|
|
14
|
+
}
|
|
13
15
|
|
|
14
16
|
interface Props extends /* @vue-ignore */ InstanceType<typeof VTextField['$props']> {
|
|
15
17
|
accept?: string
|
|
16
18
|
multiple?: boolean
|
|
17
19
|
maxSize?: number
|
|
18
|
-
modelValue?:
|
|
20
|
+
modelValue?: Base64String | Base64String[]
|
|
19
21
|
downloadable?: boolean
|
|
20
|
-
autoHydrate?: boolean
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const props = withDefaults(defineProps<Props>(), {
|
|
24
25
|
accept: '*',
|
|
25
26
|
multiple: false,
|
|
26
27
|
maxSize: 5,
|
|
27
|
-
downloadable: false
|
|
28
|
-
autoHydrate: false,
|
|
28
|
+
downloadable: false
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
const emit = defineEmits<{
|
|
32
|
-
(e: 'update:modelValue', value:
|
|
32
|
+
(e: 'update:modelValue', value: Base64String | Base64String[]): void
|
|
33
33
|
}>()
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const assets = ref<Base64File[]>([]) // items with server id (or ones we keep as “assets”)
|
|
40
|
-
const files = shallowRef<File[]>([]) // native File objects picked by the user
|
|
35
|
+
const allFiles = ref<File[]>([])
|
|
36
|
+
const allAssets = ref<Base64String[]>([])
|
|
37
|
+
const combinedBase64String = ref<Base64String[] | Base64String>([])
|
|
38
|
+
const fileInput = ref()
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
function openWindowUpload() {
|
|
41
|
+
if (props.multiple || (!allFiles.value?.length && !allAssets.value?.length)) {
|
|
42
|
+
fileInput.value?.click()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const name = x.fileName ?? ''
|
|
52
|
-
const len = x.base64String?.length ?? 0
|
|
53
|
-
return `n:${name}|l:${len}`
|
|
46
|
+
function addFiles(files: File | File[]) {
|
|
47
|
+
if (Array.isArray(files)) allFiles.value?.push(...files)
|
|
48
|
+
else allFiles.value?.push(files)
|
|
54
49
|
}
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const out: Base64File[] = []
|
|
60
|
-
for (const it of arr) {
|
|
61
|
-
const k = base64FileKey(it)
|
|
62
|
-
if (!seen.has(k)) {
|
|
63
|
-
seen.add(k)
|
|
64
|
-
out.push(it)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return out
|
|
51
|
+
function removeFileByIndex(i: number | string) {
|
|
52
|
+
const index = Number(i)
|
|
53
|
+
if (index >= 0 && index < allFiles.value.length) allFiles.value.splice(index, 1)
|
|
68
54
|
}
|
|
69
55
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
if (a.length !== b.length) return false
|
|
74
|
-
for (let i = 0; i < a.length; i++) {
|
|
75
|
-
if (base64FileKey(a[i]) !== base64FileKey(b[i])) return false
|
|
76
|
-
}
|
|
77
|
-
return true
|
|
56
|
+
function removeAssetByIndex(i: number | string) {
|
|
57
|
+
const index = Number(i)
|
|
58
|
+
if (index >= 0 && index < allAssets.value.length) allAssets.value.splice(index, 1)
|
|
78
59
|
}
|
|
79
60
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const mv = props.modelValue
|
|
83
|
-
if (!mv) {
|
|
84
|
-
assets.value = []
|
|
85
|
-
files.value = []
|
|
86
|
-
return
|
|
87
|
-
}
|
|
61
|
+
function fileToBase64(file: File) {
|
|
62
|
+
const maxSize = props.maxSize * 1048576
|
|
88
63
|
|
|
89
|
-
|
|
64
|
+
return new Promise<Base64String>((resolve, reject) => {
|
|
65
|
+
if (file.size > maxSize) reject(`File (${file.name}) size exceeds the ${props.maxSize} MB limit.`)
|
|
90
66
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
67
|
+
const reader = new FileReader()
|
|
68
|
+
reader.onload = (event) => {
|
|
69
|
+
resolve({ fileName: file.name, base64String: event.target?.result as string })
|
|
70
|
+
}
|
|
71
|
+
reader.onerror = reject
|
|
72
|
+
reader.readAsDataURL(file)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
94
75
|
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (f) inlineFiles.push(f)
|
|
100
|
-
}
|
|
76
|
+
function base64ToFile(base64Data: string, filename: string, defaultContentType: string = "application/octet-stream") {
|
|
77
|
+
const matchResult = base64Data.match(/data:([^;]*);base64,(.*)/);
|
|
78
|
+
let contentType: string;
|
|
79
|
+
let base64Payload: string;
|
|
101
80
|
|
|
102
|
-
|
|
103
|
-
|
|
81
|
+
if (matchResult) {
|
|
82
|
+
[contentType, base64Payload] = matchResult.slice(1)
|
|
83
|
+
} else {
|
|
84
|
+
contentType = defaultContentType
|
|
85
|
+
base64Payload = base64Data
|
|
86
|
+
}
|
|
104
87
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
88
|
+
try {
|
|
89
|
+
const binaryStr = atob(base64Payload)
|
|
90
|
+
const bytes = new Uint8Array(binaryStr.length)
|
|
91
|
+
for (let i = 0; i < binaryStr.length; i++) {
|
|
92
|
+
bytes[i] = binaryStr.charCodeAt(i)
|
|
110
93
|
}
|
|
94
|
+
return new File([bytes], filename, { type: contentType });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error("Invalid base64 data", error);
|
|
97
|
+
return undefined;
|
|
111
98
|
}
|
|
112
99
|
}
|
|
113
100
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (props.multiple || (assets.value.length === 0 && files.value.length === 0)) {
|
|
117
|
-
fileInput.value?.click()
|
|
118
|
-
}
|
|
119
|
-
}
|
|
101
|
+
function downloadBase64File(base64Data: string, filename: string): void {
|
|
102
|
+
const file = base64ToFile(base64Data,filename)
|
|
120
103
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
104
|
+
if (file) {
|
|
105
|
+
const link = document.createElement("a");
|
|
106
|
+
link.href = URL.createObjectURL(file);
|
|
107
|
+
link.download = filename;
|
|
126
108
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (idx >= 0 && idx < files.value.length) {
|
|
131
|
-
const copy = files.value.slice()
|
|
132
|
-
copy.splice(idx, 1)
|
|
133
|
-
files.value = copy
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function removeAssetByIndex(i: number | string) {
|
|
137
|
-
const idx = Number(i)
|
|
138
|
-
if (idx >= 0 && idx < assets.value.length) {
|
|
139
|
-
const copy = assets.value.slice()
|
|
140
|
-
copy.splice(idx, 1)
|
|
141
|
-
assets.value = copy
|
|
142
|
-
}
|
|
143
|
-
}
|
|
109
|
+
// Append the link to the body temporarily and trigger the download
|
|
110
|
+
document.body.appendChild(link);
|
|
111
|
+
link.click();
|
|
144
112
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (!p) {
|
|
150
|
-
p = fileToBase64WithMaxSize(f)
|
|
151
|
-
fileCache.set(f, p)
|
|
152
|
-
}
|
|
153
|
-
return p
|
|
154
|
-
})
|
|
155
|
-
return Promise.all(tasks)
|
|
113
|
+
// Cleanup
|
|
114
|
+
document.body.removeChild(link);
|
|
115
|
+
URL.revokeObjectURL(link.href);
|
|
116
|
+
}
|
|
156
117
|
}
|
|
157
118
|
|
|
158
|
-
/** Combined result (array form) */
|
|
159
|
-
const combinedArray = ref<Base64File[]>([])
|
|
160
|
-
|
|
161
|
-
/** Dirty flag for <v-text-field> */
|
|
162
|
-
const isDirty = computed(() =>
|
|
163
|
-
props.multiple ? combinedArray.value.length > 0 : combinedArray.value[0] != null
|
|
164
|
-
)
|
|
165
119
|
|
|
166
|
-
/** Sync from props.modelValue (guarded) */
|
|
167
120
|
watch(
|
|
168
121
|
() => props.modelValue,
|
|
169
122
|
() => {
|
|
170
|
-
if (
|
|
171
|
-
|
|
123
|
+
if (Array.isArray(props.modelValue)) {
|
|
124
|
+
allAssets.value = props.modelValue.filter((item) => item.id !== undefined)
|
|
125
|
+
allFiles.value = props.modelValue
|
|
126
|
+
.filter((item) => item.id === undefined && item.base64String !== undefined)
|
|
127
|
+
.map((base64) => base64ToFile(base64.base64String as string, base64.fileName))
|
|
128
|
+
.filter((item) => item !== undefined) as File[]
|
|
129
|
+
} else if (props.modelValue) {
|
|
130
|
+
allAssets.value = props.modelValue.id !== undefined ? [props.modelValue] : []
|
|
131
|
+
allFiles.value =
|
|
132
|
+
props.modelValue.id === undefined && props.modelValue.base64String !== undefined
|
|
133
|
+
? [base64ToFile(props.modelValue.base64String, props.modelValue.fileName)].filter(
|
|
134
|
+
(item) => item !== undefined
|
|
135
|
+
) as File[]
|
|
136
|
+
: []
|
|
137
|
+
} else {
|
|
138
|
+
allAssets.value = []
|
|
139
|
+
allFiles.value = []
|
|
140
|
+
}
|
|
172
141
|
},
|
|
173
142
|
{ deep: true, immediate: true }
|
|
174
143
|
)
|
|
175
144
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
if (props.multiple) {
|
|
195
|
-
emit('update:modelValue', combinedArray.value)
|
|
196
|
-
} else {
|
|
197
|
-
emit('update:modelValue', combinedArray.value[0] ?? null)
|
|
198
|
-
}
|
|
199
|
-
} finally {
|
|
200
|
-
// let Vue flush the emit before re-enabling the external sync
|
|
201
|
-
queueMicrotask(() => { internalSync = false })
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
} catch (error: any) {
|
|
205
|
-
alert?.addAlert({ message: String(error), alertType: 'error' })
|
|
206
|
-
files.value = []
|
|
207
|
-
const fallback = props.multiple
|
|
208
|
-
? [...assets.value]
|
|
209
|
-
: (assets.value[0] ? [assets.value[0]] : [])
|
|
210
|
-
|
|
211
|
-
if (!arraysEqualByKey(combinedArray.value, fallback)) {
|
|
212
|
-
combinedArray.value = fallback
|
|
213
|
-
internalSync = true
|
|
214
|
-
try {
|
|
215
|
-
if (props.multiple) emit('update:modelValue', combinedArray.value)
|
|
216
|
-
else emit('update:modelValue', combinedArray.value[0] ?? null)
|
|
217
|
-
} finally {
|
|
218
|
-
queueMicrotask(() => { internalSync = false })
|
|
219
|
-
}
|
|
220
|
-
}
|
|
145
|
+
watch([allAssets, allFiles], () => {
|
|
146
|
+
if (allFiles.value.length) {
|
|
147
|
+
const base64Promises = allFiles.value.map(fileToBase64)
|
|
148
|
+
Promise.all(base64Promises)
|
|
149
|
+
.then((base64Strings) => {
|
|
150
|
+
combinedBase64String.value = props.multiple
|
|
151
|
+
? [...allAssets.value, ...base64Strings]
|
|
152
|
+
: base64Strings[0] || allAssets.value[0] || null
|
|
153
|
+
})
|
|
154
|
+
.catch((error) => {
|
|
155
|
+
alert?.addAlert({ message: error, alertType: 'error' })
|
|
156
|
+
allFiles.value = []
|
|
157
|
+
})
|
|
158
|
+
} else {
|
|
159
|
+
combinedBase64String.value = props.multiple
|
|
160
|
+
? [...allAssets.value]
|
|
161
|
+
: allAssets.value[0] || null
|
|
221
162
|
}
|
|
222
163
|
}, { deep: true, immediate: true })
|
|
164
|
+
|
|
165
|
+
watch(combinedBase64String, (newValue, oldValue) => {
|
|
166
|
+
if (!isEqual(newValue, oldValue)) {
|
|
167
|
+
emit('update:modelValue', props.multiple ? uniqWith(newValue as Base64String[], isEqual) : newValue)
|
|
168
|
+
}
|
|
169
|
+
}, { deep: true })
|
|
223
170
|
</script>
|
|
224
171
|
|
|
225
172
|
<template>
|
|
@@ -227,36 +174,28 @@ watch([assets, files], async () => {
|
|
|
227
174
|
v-bind="$attrs"
|
|
228
175
|
label="Upload files"
|
|
229
176
|
readonly
|
|
230
|
-
:dirty="
|
|
231
|
-
v-on="
|
|
177
|
+
:dirty="Array.isArray(combinedBase64String) ? combinedBase64String.length > 0 : !!combinedBase64String"
|
|
178
|
+
v-on="Array.isArray(combinedBase64String) && combinedBase64String.length > 0 ? {} : { click: openWindowUpload }"
|
|
232
179
|
>
|
|
233
180
|
<template #default>
|
|
234
|
-
<!-- Server/asset items -->
|
|
235
181
|
<v-chip
|
|
236
|
-
v-for="(asset, index) in
|
|
237
|
-
:key="
|
|
182
|
+
v-for="(asset, index) in allAssets"
|
|
183
|
+
:key="asset.fileName"
|
|
238
184
|
color="green"
|
|
239
185
|
variant="flat"
|
|
240
186
|
closable
|
|
241
187
|
@click:close="removeAssetByIndex(index)"
|
|
242
188
|
>
|
|
243
189
|
{{ asset.originalFileName || asset.fileName }}
|
|
244
|
-
<template #append v-if="
|
|
190
|
+
<template #append v-if="downloadable">
|
|
245
191
|
<slot name="download" :item="asset">
|
|
246
|
-
<v-icon
|
|
247
|
-
v-if="asset.base64String"
|
|
248
|
-
@click.stop="downloadBase64File(asset.base64String, asset.originalFileName || asset.fileName)"
|
|
249
|
-
>
|
|
250
|
-
mdi mdi-download
|
|
251
|
-
</v-icon>
|
|
192
|
+
<v-icon @click="downloadBase64File(asset.base64String || '',asset.originalFileName || asset.fileName)" v-if="asset.base64String">mdi mdi-download</v-icon>
|
|
252
193
|
</slot>
|
|
253
194
|
</template>
|
|
254
195
|
</v-chip>
|
|
255
|
-
|
|
256
|
-
<!-- Local uploaded files -->
|
|
257
196
|
<v-chip
|
|
258
|
-
v-for="(file, index) in
|
|
259
|
-
:key="
|
|
197
|
+
v-for="(file, index) in allFiles"
|
|
198
|
+
:key="file.name"
|
|
260
199
|
color="primary"
|
|
261
200
|
variant="flat"
|
|
262
201
|
closable
|
|
@@ -266,14 +205,12 @@ watch([assets, files], async () => {
|
|
|
266
205
|
</v-chip>
|
|
267
206
|
</template>
|
|
268
207
|
|
|
269
|
-
|
|
270
|
-
<template v-if="props.multiple && combinedArray.length > 0" #append-inner>
|
|
208
|
+
<template v-if="props.multiple && Array.isArray(combinedBase64String) && combinedBase64String.length > 0" #append-inner>
|
|
271
209
|
<VBtn variant="text" :icon="true" @click="openWindowUpload">
|
|
272
210
|
<v-icon>mdi mdi-plus</v-icon>
|
|
273
211
|
</VBtn>
|
|
274
212
|
</template>
|
|
275
213
|
</v-text-field>
|
|
276
|
-
|
|
277
214
|
<v-file-input
|
|
278
215
|
ref="fileInput"
|
|
279
216
|
:accept="props.accept"
|
|
@@ -281,4 +218,4 @@ watch([assets, files], async () => {
|
|
|
281
218
|
style="display: none"
|
|
282
219
|
@update:model-value="addFiles"
|
|
283
220
|
/>
|
|
284
|
-
</template>
|
|
221
|
+
</template>
|
|
@@ -497,8 +497,6 @@ defineExpose({
|
|
|
497
497
|
show-first-last-page
|
|
498
498
|
@first="footerProps.setPage(1)"
|
|
499
499
|
@last="footerProps.setPage(footerProps.pageCount)"
|
|
500
|
-
@next="footerProps.nextPage"
|
|
501
|
-
@prev="footerProps.prevPage"
|
|
502
500
|
@update:model-value="footerProps.setPage"
|
|
503
501
|
/>
|
|
504
502
|
</v-row>
|
|
@@ -34,6 +34,7 @@ interface Props {
|
|
|
34
34
|
parentTemplates?: string|string[]
|
|
35
35
|
dirtyClass?: string
|
|
36
36
|
dirtyOnCreate?: boolean
|
|
37
|
+
sanitizeDelay?: number
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -43,7 +44,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
43
44
|
decoration: () => { return {} },
|
|
44
45
|
parentTemplates: (): string[] => [],
|
|
45
46
|
dirtyClass: "form-data-dirty",
|
|
46
|
-
dirtyOnCreate: false
|
|
47
|
+
dirtyOnCreate: false,
|
|
48
|
+
sanitizeDelay: 2000,
|
|
47
49
|
})
|
|
48
50
|
|
|
49
51
|
const emit = defineEmits(['update:modelValue'])
|
|
@@ -87,7 +89,7 @@ function isBlankString(v: unknown): v is string {
|
|
|
87
89
|
return isString(v) && v.trim().length === 0
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
const sanitizeBlankStrings = debounce(sanitizeBlankStringsRaw,
|
|
92
|
+
const sanitizeBlankStrings = debounce(sanitizeBlankStringsRaw, props.sanitizeDelay)
|
|
91
93
|
|
|
92
94
|
function sanitizeBlankStringsRaw(val: any, original?: any): void {
|
|
93
95
|
if (!original && props.originalData) {
|
|
@@ -125,6 +127,71 @@ function sanitizeBlankStringsRaw(val: any, original?: any): void {
|
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
function autoSanitizedDisplay(item: any, separator: string = ","): string | undefined {
|
|
131
|
+
const isEmptyScalar = (v: any) =>
|
|
132
|
+
v === null ||
|
|
133
|
+
v === undefined ||
|
|
134
|
+
(typeof v === "string" && v.trim() === "") ||
|
|
135
|
+
(typeof v === "number" && Number.isNaN(v));
|
|
136
|
+
|
|
137
|
+
const toStr = (v: any): string | undefined => {
|
|
138
|
+
if (isEmptyScalar(v)) return undefined;
|
|
139
|
+
if (typeof v === "string") return v.trim();
|
|
140
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
|
|
141
|
+
if (typeof v === "symbol") return v.description ?? String(v);
|
|
142
|
+
if (typeof v === "function") return v.name ? `[Function ${v.name}]` : "[Function]";
|
|
143
|
+
return undefined;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// 1) empty -> undefined
|
|
147
|
+
if (isEmptyScalar(item)) return undefined;
|
|
148
|
+
|
|
149
|
+
// 2) array -> recurse + join
|
|
150
|
+
if (Array.isArray(item)) {
|
|
151
|
+
const parts = item
|
|
152
|
+
.map((x) => autoSanitizedDisplay(x, separator))
|
|
153
|
+
.filter((s): s is string => typeof s === "string" && s.trim() !== "");
|
|
154
|
+
return parts.length ? parts.join(separator) : undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// simple scalars
|
|
158
|
+
const scalar = toStr(item);
|
|
159
|
+
if (scalar !== undefined) return scalar;
|
|
160
|
+
|
|
161
|
+
// 3) object
|
|
162
|
+
if (typeof item === "object") {
|
|
163
|
+
// 3.1 label
|
|
164
|
+
if ("label" in item) {
|
|
165
|
+
const v = autoSanitizedDisplay((item as any).label, separator) ?? toStr((item as any).label);
|
|
166
|
+
if (v !== undefined) return v;
|
|
167
|
+
}
|
|
168
|
+
// 3.2 value
|
|
169
|
+
if ("value" in item) {
|
|
170
|
+
const v = autoSanitizedDisplay((item as any).value, separator) ?? toStr((item as any).value);
|
|
171
|
+
if (v !== undefined) return v;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 3.3 stringify attributes as key:value (recurse values)
|
|
175
|
+
const entries = Object.entries(item as Record<string, any>)
|
|
176
|
+
.map(([k, v]) => {
|
|
177
|
+
const rendered = autoSanitizedDisplay(v, separator) ?? toStr(v);
|
|
178
|
+
if (rendered === undefined || rendered.trim() === "") return undefined;
|
|
179
|
+
return `${k}:${rendered}`;
|
|
180
|
+
})
|
|
181
|
+
.filter((x): x is string => typeof x === "string" && x.trim() !== "");
|
|
182
|
+
|
|
183
|
+
return entries.length ? entries.join(separator) : undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// fallback
|
|
187
|
+
try {
|
|
188
|
+
const s = String(item);
|
|
189
|
+
return s.trim() ? s : undefined;
|
|
190
|
+
} catch {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
128
195
|
watch(formData, (newValue) => {
|
|
129
196
|
sanitizeBlankStrings(newValue)
|
|
130
197
|
emit('update:modelValue', newValue)
|
|
@@ -219,6 +286,7 @@ function buildFormComponent() {
|
|
|
219
286
|
resetValidate: () => formPadTemplate.value.resetValidate(),
|
|
220
287
|
isValid,
|
|
221
288
|
...templateScriptFunction.value(props, ctx, ...Object.values(vueFunctions)),
|
|
289
|
+
autoSanitizedDisplay
|
|
222
290
|
}
|
|
223
291
|
},
|
|
224
292
|
template: componentTemplate,
|
|
@@ -279,6 +347,7 @@ defineExpose({
|
|
|
279
347
|
:disabled="disabled"
|
|
280
348
|
:readonly="readonly"
|
|
281
349
|
:class="$attrs.class"
|
|
350
|
+
autocomplete="off"
|
|
282
351
|
>
|
|
283
352
|
<template #default="formProvided">
|
|
284
353
|
<slot
|