tantee-nuxt-commons 0.0.169 → 0.0.170
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 +0 -1
- 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 +189 -126
- package/dist/runtime/components/form/Iterator.vue +2 -0
- package/dist/runtime/components/form/Pad.vue +3 -72
- package/dist/runtime/components/form/SignPad.vue +126 -141
- package/dist/runtime/components/form/images/Field.vue +197 -168
- package/dist/runtime/components/label/DateCount.vue +13 -117
- package/dist/runtime/components/pdf/View.vue +32 -42
- package/dist/runtime/composables/assetFile.d.ts +31 -0
- package/dist/runtime/composables/assetFile.js +134 -0
- package/dist/runtime/composables/document/templateFormTable.js +1 -4
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -45,7 +45,6 @@ watch(() => alert?.hasAlert(), (hasAlert) => {
|
|
|
45
45
|
elevation="2"
|
|
46
46
|
theme="dark"
|
|
47
47
|
variant="flat"
|
|
48
|
-
v-bind="currentItem.alertIcon ? { icon: currentItem.alertIcon } : {}"
|
|
49
48
|
@click:close="renewAlert()"
|
|
50
49
|
>
|
|
51
50
|
{{ 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({},
|
|
88
|
+
formData.value = Object.assign({}, props.initialData)
|
|
89
89
|
}
|
|
90
90
|
isSavedAndStay.value = false
|
|
91
91
|
}
|
|
@@ -1,172 +1,225 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import {
|
|
3
|
-
import {ref, watch} from 'vue'
|
|
4
|
-
import {VTextField} from 'vuetify/components/VTextField'
|
|
5
|
-
import {useAlert} from '../../composables/alert'
|
|
2
|
+
import { castArray } from 'lodash-es'
|
|
3
|
+
import { ref, shallowRef, watch, computed } from 'vue'
|
|
4
|
+
import { VTextField } from 'vuetify/components/VTextField'
|
|
5
|
+
import { useAlert } from '../../composables/alert'
|
|
6
|
+
import { type Base64File } from '../../composables/assetFile'
|
|
7
|
+
import { useAssetFile } from '../../composables/assetFile'
|
|
6
8
|
|
|
7
9
|
const alert = useAlert()
|
|
10
|
+
const { hydrateAssetFile, base64ToFile, fileToBase64, downloadBase64File } = useAssetFile()
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
base64String?: string
|
|
11
|
-
fileName: string
|
|
12
|
-
originalFileName?: string
|
|
13
|
-
id?: number
|
|
14
|
-
}
|
|
12
|
+
const fileToBase64WithMaxSize = (file: File) => fileToBase64(file, props.maxSize)
|
|
15
13
|
|
|
16
14
|
interface Props extends /* @vue-ignore */ InstanceType<typeof VTextField['$props']> {
|
|
17
15
|
accept?: string
|
|
18
16
|
multiple?: boolean
|
|
19
17
|
maxSize?: number
|
|
20
|
-
modelValue?:
|
|
18
|
+
modelValue?: Base64File | Base64File[] | null
|
|
21
19
|
downloadable?: boolean
|
|
20
|
+
autoHydrate?: boolean
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
const props = withDefaults(defineProps<Props>(), {
|
|
25
24
|
accept: '*',
|
|
26
25
|
multiple: false,
|
|
27
26
|
maxSize: 5,
|
|
28
|
-
downloadable: false
|
|
27
|
+
downloadable: false,
|
|
28
|
+
autoHydrate: false,
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
const emit = defineEmits<{
|
|
32
|
-
(e: 'update:modelValue', value:
|
|
32
|
+
(e: 'update:modelValue', value: Base64File | Base64File[] | null): void
|
|
33
33
|
}>()
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const combinedBase64String = ref<Base64String[] | Base64String>([])
|
|
38
|
-
const fileInput = ref()
|
|
35
|
+
/** UI ref */
|
|
36
|
+
const fileInput = ref<HTMLInputElement | null>(null)
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
}
|
|
38
|
+
/** Internal sources (always arrays) */
|
|
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
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
else allFiles.value?.push(files)
|
|
49
|
-
}
|
|
42
|
+
/** Cache to avoid re-reading the same File repeatedly */
|
|
43
|
+
const fileCache = new WeakMap<File, Promise<Base64File>>()
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
/** Re-entrancy guard to break the emit -> props watcher -> syncFromModel loop */
|
|
46
|
+
let internalSync = false
|
|
47
|
+
|
|
48
|
+
/** Build a stable dedupe key */
|
|
49
|
+
function base64FileKey(x: Base64File): string {
|
|
50
|
+
if (x.id != null) return `id:${x.id}`
|
|
51
|
+
const name = x.fileName ?? ''
|
|
52
|
+
const len = x.base64String?.length ?? 0
|
|
53
|
+
return `n:${name}|l:${len}`
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
/** Dedupe by key (preserves order) */
|
|
57
|
+
function uniqByKey(arr: Base64File[]): Base64File[] {
|
|
58
|
+
const seen = new Set<string>()
|
|
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
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
/** Shallow equal by key sequence */
|
|
71
|
+
function arraysEqualByKey(a: Base64File[], b: Base64File[]): boolean {
|
|
72
|
+
if (a === b) return true
|
|
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
|
|
78
|
+
}
|
|
63
79
|
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
/** Normalize incoming modelValue into arrays for internal use */
|
|
81
|
+
async function syncFromModel() {
|
|
82
|
+
const mv = props.modelValue
|
|
83
|
+
if (!mv) {
|
|
84
|
+
assets.value = []
|
|
85
|
+
files.value = []
|
|
86
|
+
return
|
|
87
|
+
}
|
|
66
88
|
|
|
67
|
-
|
|
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
|
-
}
|
|
89
|
+
const asArray = castArray(mv) as Base64File[]
|
|
75
90
|
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
let base64Payload: string;
|
|
91
|
+
// Split into “asset with id” vs “inline base64” (no id)
|
|
92
|
+
const incomingAssets = asArray.filter(a => a.id != null)
|
|
93
|
+
const inlineBase64 = asArray.filter(a => a.id == null && a.base64String)
|
|
80
94
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
// Convert inline base64 to File for consistent UX
|
|
96
|
+
const inlineFiles: File[] = []
|
|
97
|
+
for (const item of inlineBase64) {
|
|
98
|
+
const f = base64ToFile(item.base64String as string, item.fileName)
|
|
99
|
+
if (f) inlineFiles.push(f)
|
|
86
100
|
}
|
|
87
101
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
assets.value = incomingAssets
|
|
103
|
+
files.value = inlineFiles
|
|
104
|
+
|
|
105
|
+
// Optionally hydrate assets that need it
|
|
106
|
+
if (props.autoHydrate) {
|
|
107
|
+
const needsHydration = assets.value.filter(a => a.id != null && !a.base64String)
|
|
108
|
+
if (needsHydration.length) {
|
|
109
|
+
await Promise.allSettled(needsHydration.map(hydrateAssetFile))
|
|
93
110
|
}
|
|
94
|
-
return new File([bytes], filename, { type: contentType });
|
|
95
|
-
} catch (error) {
|
|
96
|
-
console.error("Invalid base64 data", error);
|
|
97
|
-
return undefined;
|
|
98
111
|
}
|
|
99
112
|
}
|
|
100
113
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
link.download = filename;
|
|
114
|
+
/** Open chooser */
|
|
115
|
+
function openWindowUpload() {
|
|
116
|
+
if (props.multiple || (assets.value.length === 0 && files.value.length === 0)) {
|
|
117
|
+
fileInput.value?.click()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
108
120
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
/** Add files from input (single or multiple) */
|
|
122
|
+
function addFiles(payload: File | File[]) {
|
|
123
|
+
if (Array.isArray(payload)) files.value = [...files.value, ...payload]
|
|
124
|
+
else files.value = [...files.value, payload]
|
|
125
|
+
}
|
|
112
126
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
/** Remove chips */
|
|
128
|
+
function removeFileByIndex(i: number | string) {
|
|
129
|
+
const idx = Number(i)
|
|
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
|
|
116
142
|
}
|
|
117
143
|
}
|
|
118
144
|
|
|
145
|
+
/** Convert current files → Base64File[] (cached) */
|
|
146
|
+
async function filesToBase64Files(list: File[]): Promise<Base64File[]> {
|
|
147
|
+
const tasks = list.map(f => {
|
|
148
|
+
let p = fileCache.get(f)
|
|
149
|
+
if (!p) {
|
|
150
|
+
p = fileToBase64WithMaxSize(f)
|
|
151
|
+
fileCache.set(f, p)
|
|
152
|
+
}
|
|
153
|
+
return p
|
|
154
|
+
})
|
|
155
|
+
return Promise.all(tasks)
|
|
156
|
+
}
|
|
119
157
|
|
|
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
|
+
|
|
166
|
+
/** Sync from props.modelValue (guarded) */
|
|
120
167
|
watch(
|
|
121
168
|
() => props.modelValue,
|
|
122
169
|
() => {
|
|
123
|
-
if (
|
|
124
|
-
|
|
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
|
-
}
|
|
170
|
+
if (internalSync) return
|
|
171
|
+
void syncFromModel()
|
|
141
172
|
},
|
|
142
173
|
{ deep: true, immediate: true }
|
|
143
174
|
)
|
|
144
175
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
176
|
+
/** Rebuild combined and emit (guarded, deduped, only-on-change) */
|
|
177
|
+
watch([assets, files], async () => {
|
|
178
|
+
try {
|
|
179
|
+
const base64FromFiles = await filesToBase64Files(files.value)
|
|
180
|
+
|
|
181
|
+
const mergedArray = props.multiple
|
|
182
|
+
? [...assets.value, ...base64FromFiles]
|
|
183
|
+
: (assets.value[0] ?? base64FromFiles[0] ?? null)
|
|
184
|
+
? [assets.value[0] ?? base64FromFiles[0]]
|
|
185
|
+
: []
|
|
186
|
+
|
|
187
|
+
const nextCombined = props.multiple ? uniqByKey(mergedArray) : mergedArray
|
|
188
|
+
|
|
189
|
+
if (!arraysEqualByKey(combinedArray.value, nextCombined)) {
|
|
190
|
+
combinedArray.value = nextCombined
|
|
191
|
+
|
|
192
|
+
internalSync = true
|
|
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]] : [])
|
|
164
210
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
}
|
|
168
221
|
}
|
|
169
|
-
}, { deep: true })
|
|
222
|
+
}, { deep: true, immediate: true })
|
|
170
223
|
</script>
|
|
171
224
|
|
|
172
225
|
<template>
|
|
@@ -174,28 +227,36 @@ watch(combinedBase64String, (newValue, oldValue) => {
|
|
|
174
227
|
v-bind="$attrs"
|
|
175
228
|
label="Upload files"
|
|
176
229
|
readonly
|
|
177
|
-
:dirty="
|
|
178
|
-
v-on="
|
|
230
|
+
:dirty="isDirty"
|
|
231
|
+
v-on="isDirty && !props.multiple ? {} : { click: openWindowUpload }"
|
|
179
232
|
>
|
|
180
233
|
<template #default>
|
|
234
|
+
<!-- Server/asset items -->
|
|
181
235
|
<v-chip
|
|
182
|
-
v-for="(asset, index) in
|
|
183
|
-
:key="asset.fileName"
|
|
236
|
+
v-for="(asset, index) in assets"
|
|
237
|
+
:key="`${asset.id ?? asset.fileName}-${index}`"
|
|
184
238
|
color="green"
|
|
185
239
|
variant="flat"
|
|
186
240
|
closable
|
|
187
241
|
@click:close="removeAssetByIndex(index)"
|
|
188
242
|
>
|
|
189
243
|
{{ asset.originalFileName || asset.fileName }}
|
|
190
|
-
<template #append v-if="downloadable">
|
|
244
|
+
<template #append v-if="props.downloadable">
|
|
191
245
|
<slot name="download" :item="asset">
|
|
192
|
-
<v-icon
|
|
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>
|
|
193
252
|
</slot>
|
|
194
253
|
</template>
|
|
195
254
|
</v-chip>
|
|
255
|
+
|
|
256
|
+
<!-- Local uploaded files -->
|
|
196
257
|
<v-chip
|
|
197
|
-
v-for="(file, index) in
|
|
198
|
-
:key="file.name"
|
|
258
|
+
v-for="(file, index) in files"
|
|
259
|
+
:key="`${file.name}-${file.size}-${index}`"
|
|
199
260
|
color="primary"
|
|
200
261
|
variant="flat"
|
|
201
262
|
closable
|
|
@@ -205,12 +266,14 @@ watch(combinedBase64String, (newValue, oldValue) => {
|
|
|
205
266
|
</v-chip>
|
|
206
267
|
</template>
|
|
207
268
|
|
|
208
|
-
|
|
269
|
+
<!-- Add more button for multi mode -->
|
|
270
|
+
<template v-if="props.multiple && combinedArray.length > 0" #append-inner>
|
|
209
271
|
<VBtn variant="text" :icon="true" @click="openWindowUpload">
|
|
210
272
|
<v-icon>mdi mdi-plus</v-icon>
|
|
211
273
|
</VBtn>
|
|
212
274
|
</template>
|
|
213
275
|
</v-text-field>
|
|
276
|
+
|
|
214
277
|
<v-file-input
|
|
215
278
|
ref="fileInput"
|
|
216
279
|
:accept="props.accept"
|
|
@@ -218,4 +281,4 @@ watch(combinedBase64String, (newValue, oldValue) => {
|
|
|
218
281
|
style="display: none"
|
|
219
282
|
@update:model-value="addFiles"
|
|
220
283
|
/>
|
|
221
|
-
</template>
|
|
284
|
+
</template>
|
|
@@ -497,6 +497,8 @@ 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"
|
|
500
502
|
@update:model-value="footerProps.setPage"
|
|
501
503
|
/>
|
|
502
504
|
</v-row>
|
|
@@ -34,7 +34,6 @@ interface Props {
|
|
|
34
34
|
parentTemplates?: string|string[]
|
|
35
35
|
dirtyClass?: string
|
|
36
36
|
dirtyOnCreate?: boolean
|
|
37
|
-
sanitizeDelay?: number
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -44,8 +43,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
44
43
|
decoration: () => { return {} },
|
|
45
44
|
parentTemplates: (): string[] => [],
|
|
46
45
|
dirtyClass: "form-data-dirty",
|
|
47
|
-
dirtyOnCreate: false
|
|
48
|
-
sanitizeDelay: 2000,
|
|
46
|
+
dirtyOnCreate: false
|
|
49
47
|
})
|
|
50
48
|
|
|
51
49
|
const emit = defineEmits(['update:modelValue'])
|
|
@@ -89,7 +87,7 @@ function isBlankString(v: unknown): v is string {
|
|
|
89
87
|
return isString(v) && v.trim().length === 0
|
|
90
88
|
}
|
|
91
89
|
|
|
92
|
-
const sanitizeBlankStrings = debounce(sanitizeBlankStringsRaw,
|
|
90
|
+
const sanitizeBlankStrings = debounce(sanitizeBlankStringsRaw, 500)
|
|
93
91
|
|
|
94
92
|
function sanitizeBlankStringsRaw(val: any, original?: any): void {
|
|
95
93
|
if (!original && props.originalData) {
|
|
@@ -127,71 +125,6 @@ function sanitizeBlankStringsRaw(val: any, original?: any): void {
|
|
|
127
125
|
}
|
|
128
126
|
}
|
|
129
127
|
|
|
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
|
-
|
|
195
128
|
watch(formData, (newValue) => {
|
|
196
129
|
sanitizeBlankStrings(newValue)
|
|
197
130
|
emit('update:modelValue', newValue)
|
|
@@ -286,7 +219,6 @@ function buildFormComponent() {
|
|
|
286
219
|
resetValidate: () => formPadTemplate.value.resetValidate(),
|
|
287
220
|
isValid,
|
|
288
221
|
...templateScriptFunction.value(props, ctx, ...Object.values(vueFunctions)),
|
|
289
|
-
autoSanitizedDisplay
|
|
290
222
|
}
|
|
291
223
|
},
|
|
292
224
|
template: componentTemplate,
|
|
@@ -347,7 +279,6 @@ defineExpose({
|
|
|
347
279
|
:disabled="disabled"
|
|
348
280
|
:readonly="readonly"
|
|
349
281
|
:class="$attrs.class"
|
|
350
|
-
autocomplete="off"
|
|
351
282
|
>
|
|
352
283
|
<template #default="formProvided">
|
|
353
284
|
<slot
|
|
@@ -386,5 +317,5 @@ defineExpose({
|
|
|
386
317
|
/>
|
|
387
318
|
</template>
|
|
388
319
|
<style>
|
|
389
|
-
.form-data-dirty
|
|
320
|
+
.form-data-dirty:not(.v-input--error) :not(.v-chip):not(.v-chip *){color:color-mix(in srgb,currentColor 70%,rgb(var(--v-theme-primary)))!important;text-shadow:0 0 .02em currentColor}
|
|
390
321
|
</style>
|