@ramathibodi/nuxt-commons 0.1.73 → 0.1.75

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 (111) hide show
  1. package/README.md +115 -96
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +1 -0
  4. package/dist/runtime/components/Alert.vue +58 -54
  5. package/dist/runtime/components/BarcodeReader.vue +130 -122
  6. package/dist/runtime/components/ExportCSV.vue +110 -102
  7. package/dist/runtime/components/FileBtn.vue +79 -67
  8. package/dist/runtime/components/ImportCSV.vue +151 -139
  9. package/dist/runtime/components/MrzReader.vue +168 -0
  10. package/dist/runtime/components/SplitterPanel.vue +67 -59
  11. package/dist/runtime/components/TabsGroup.vue +39 -31
  12. package/dist/runtime/components/TextBarcode.vue +66 -54
  13. package/dist/runtime/components/device/IdCardButton.vue +95 -83
  14. package/dist/runtime/components/device/IdCardWebSocket.vue +207 -195
  15. package/dist/runtime/components/device/Scanner.vue +350 -338
  16. package/dist/runtime/components/dialog/Confirm.vue +112 -100
  17. package/dist/runtime/components/dialog/Host.vue +88 -84
  18. package/dist/runtime/components/dialog/Index.vue +84 -72
  19. package/dist/runtime/components/dialog/Loading.vue +51 -39
  20. package/dist/runtime/components/dialog/default/Confirm.vue +112 -100
  21. package/dist/runtime/components/dialog/default/Loading.vue +60 -48
  22. package/dist/runtime/components/dialog/default/Notify.vue +82 -70
  23. package/dist/runtime/components/dialog/default/Printing.vue +46 -34
  24. package/dist/runtime/components/dialog/default/VerifyUser.vue +144 -132
  25. package/dist/runtime/components/document/Form.vue +50 -42
  26. package/dist/runtime/components/document/TemplateBuilder.vue +536 -524
  27. package/dist/runtime/components/form/ActionPad.vue +156 -144
  28. package/dist/runtime/components/form/Birthdate.vue +116 -104
  29. package/dist/runtime/components/form/CheckboxGroup.vue +99 -87
  30. package/dist/runtime/components/form/CodeEditor.vue +45 -37
  31. package/dist/runtime/components/form/Date.vue +270 -258
  32. package/dist/runtime/components/form/DateTime.vue +220 -208
  33. package/dist/runtime/components/form/Dialog.vue +178 -166
  34. package/dist/runtime/components/form/EditPad.vue +157 -145
  35. package/dist/runtime/components/form/File.vue +295 -283
  36. package/dist/runtime/components/form/Hidden.vue +44 -32
  37. package/dist/runtime/components/form/Iterator.vue +538 -526
  38. package/dist/runtime/components/form/Login.vue +143 -131
  39. package/dist/runtime/components/form/Pad.vue +399 -387
  40. package/dist/runtime/components/form/SignPad.vue +226 -218
  41. package/dist/runtime/components/form/System.vue +34 -26
  42. package/dist/runtime/components/form/Table.vue +391 -379
  43. package/dist/runtime/components/form/TableData.vue +236 -224
  44. package/dist/runtime/components/form/Time.vue +177 -165
  45. package/dist/runtime/components/form/images/Capture.vue +245 -237
  46. package/dist/runtime/components/form/images/Edit.vue +133 -121
  47. package/dist/runtime/components/form/images/Field.vue +331 -320
  48. package/dist/runtime/components/form/images/Pad.vue +54 -42
  49. package/dist/runtime/components/label/Date.vue +37 -29
  50. package/dist/runtime/components/label/DateAgo.vue +102 -94
  51. package/dist/runtime/components/label/DateCount.vue +152 -144
  52. package/dist/runtime/components/label/Field.vue +111 -103
  53. package/dist/runtime/components/label/FormatMoney.vue +37 -29
  54. package/dist/runtime/components/label/Mask.vue +46 -38
  55. package/dist/runtime/components/label/Object.vue +21 -13
  56. package/dist/runtime/components/master/Autocomplete.vue +89 -81
  57. package/dist/runtime/components/master/Combobox.vue +88 -80
  58. package/dist/runtime/components/master/RadioGroup.vue +90 -78
  59. package/dist/runtime/components/master/Select.vue +70 -62
  60. package/dist/runtime/components/master/label.vue +55 -47
  61. package/dist/runtime/components/model/Autocomplete.vue +91 -79
  62. package/dist/runtime/components/model/Combobox.vue +90 -78
  63. package/dist/runtime/components/model/Pad.vue +114 -102
  64. package/dist/runtime/components/model/Select.vue +78 -72
  65. package/dist/runtime/components/model/Table.vue +370 -358
  66. package/dist/runtime/components/model/iterator.vue +497 -489
  67. package/dist/runtime/components/model/label.vue +58 -50
  68. package/dist/runtime/components/pdf/Print.vue +75 -63
  69. package/dist/runtime/components/pdf/View.vue +146 -134
  70. package/dist/runtime/composables/alert.d.ts +4 -0
  71. package/dist/runtime/composables/api.d.ts +4 -0
  72. package/dist/runtime/composables/dialog.d.ts +1 -1
  73. package/dist/runtime/composables/document/templateFormHidden.d.ts +4 -0
  74. package/dist/runtime/composables/graphql.d.ts +1 -1
  75. package/dist/runtime/composables/graphqlModel.d.ts +9 -9
  76. package/dist/runtime/composables/graphqlModelItem.d.ts +7 -7
  77. package/dist/runtime/composables/graphqlModelOperation.d.ts +6 -6
  78. package/dist/runtime/composables/localStorageModel.d.ts +4 -0
  79. package/dist/runtime/composables/lookupList.d.ts +4 -0
  80. package/dist/runtime/composables/menu.d.ts +4 -0
  81. package/dist/runtime/composables/useMrzReader.d.ts +48 -0
  82. package/dist/runtime/composables/useMrzReader.js +423 -0
  83. package/dist/runtime/composables/useTesseract.d.ts +16 -0
  84. package/dist/runtime/composables/useTesseract.js +45 -0
  85. package/dist/runtime/composables/userPermission.d.ts +1 -1
  86. package/dist/runtime/labs/Calendar.vue +99 -99
  87. package/dist/runtime/labs/form/EditMobile.vue +152 -152
  88. package/dist/runtime/labs/form/TextFieldMask.vue +43 -43
  89. package/dist/runtime/plugins/clientConfig.d.ts +1 -1
  90. package/dist/runtime/plugins/default.d.ts +1 -1
  91. package/dist/runtime/plugins/dialogManager.d.ts +1 -1
  92. package/dist/runtime/plugins/permission.d.ts +1 -1
  93. package/dist/runtime/types/alert.d.ts +11 -11
  94. package/dist/runtime/types/clientConfig.d.ts +13 -13
  95. package/dist/runtime/types/dialogManager.d.ts +35 -35
  96. package/dist/runtime/types/formDialog.d.ts +5 -5
  97. package/dist/runtime/types/graphqlOperation.d.ts +23 -23
  98. package/dist/runtime/types/menu.d.ts +31 -31
  99. package/dist/runtime/types/modules.d.ts +7 -7
  100. package/dist/runtime/types/permission.d.ts +13 -13
  101. package/dist/runtime/utils/asset.d.ts +2 -0
  102. package/dist/runtime/utils/asset.js +49 -0
  103. package/package.json +131 -122
  104. package/scripts/enrich-vue-docs-from-ai.mjs +197 -0
  105. package/scripts/generate-ai-summary.mjs +321 -0
  106. package/scripts/generate-composables-md.mjs +129 -0
  107. package/scripts/postInstall.cjs +70 -70
  108. package/templates/.codegen/codegen.ts +32 -32
  109. package/templates/.codegen/plugin-schema-object.js +161 -161
  110. package/templates/public/tesseract/mrz.traineddata.gz +0 -0
  111. package/templates/public/tesseract/ocrb.traineddata.gz +0 -0
@@ -1,390 +1,402 @@
1
- <script lang="ts" setup>
2
- /* eslint-disable @typescript-eslint/no-explicit-any,import/no-self-import */
3
- import {
4
- compile,
5
- computed,
6
- defineComponent,
7
- defineOptions,
8
- inject,
9
- onMounted,
10
- ref,
11
- shallowRef,
12
- watch,
13
- withDefaults
14
- } from 'vue'
15
- import {watchDebounced} from '@vueuse/core'
16
- import {useRules} from '../../composables/utils/validation'
17
- import {useDocumentTemplate} from '../../composables/document/template'
18
- import { isObject, isArray, isString, isPlainObject, isEqual, debounce } from 'lodash-es'
19
- import FormPad from './Pad.vue'
20
-
21
- defineOptions({
22
- inheritAttrs: false,
23
- })
24
-
25
- interface Props {
26
- modelValue?: object
27
- originalData?: object
28
- template?: any
29
- templateScript?: string
30
- disabled?: boolean
31
- readonly?: boolean
32
- isolated?: boolean
33
- decoration?: object
34
- parentTemplates?: string|string[]
35
- dirtyClass?: string
36
- dirtyOnCreate?: boolean
37
- sanitizeDelay?: number
38
- }
39
-
40
- const props = withDefaults(defineProps<Props>(), {
41
- disabled: false,
42
- readonly: false,
43
- isolated: false,
44
- decoration: () => { return {} },
45
- parentTemplates: (): string[] => [],
46
- dirtyClass: "form-data-dirty",
47
- dirtyOnCreate: false,
48
- sanitizeDelay: 2000,
49
- })
50
-
51
- const emit = defineEmits(['update:modelValue'])
52
-
53
- const disabled = ref(props.disabled)
54
- const readonly = ref(props.readonly)
55
- const decoration = ref(props.decoration)
56
-
57
- watch(() => props.disabled, (newValue) => {
58
- disabled.value = newValue
59
- })
60
-
61
- watch(() => props.readonly, (newValue) => {
62
- readonly.value = newValue
63
- })
64
-
65
- watch(() => props.decoration, (newValue) => {
66
- decoration.value = newValue
67
- }, { deep: true })
68
-
69
- const { rules } = useRules()
70
-
71
- const trimmedTemplate = ref<string>('')
72
-
73
- const vueFunctions = { ref, shallowRef, computed, watch, onMounted }
74
-
75
- const templateScriptFunction = computed(() => {
76
- let templateScript = props.templateScript?.trim() || 'return {}'
77
- const pattern = /^\s*[{[].*[}\]]\s*$/
78
- if (pattern.test(templateScript)) templateScript = 'return {}'
79
- return Function('props', 'ctx', ...Object.keys(vueFunctions), templateScript)
80
- })
81
-
82
- const formPad = ref()
83
- const formInjectKey = Symbol.for('vuetify:form')
84
- const formInjected = ref()
85
-
86
- const formData = ref<any>({})
87
-
88
- function isBlankString(v: unknown): v is string {
89
- return isString(v) && v.trim().length === 0
90
- }
91
-
92
- const sanitizeBlankStrings = debounce(sanitizeBlankStringsRaw, props.sanitizeDelay)
93
-
94
- function sanitizeBlankStringsRaw(val: any, original?: any): void {
95
- if (!original && props.originalData) {
96
- sanitizeBlankStrings(val, props.originalData)
97
- return
98
- }
99
-
100
- if (isArray(val)) {
101
- for (let i = val.length - 1; i >= 0; i--) {
102
- const item = val[i]
103
- if (isBlankString(item)) {
104
- val.splice(i, 1)
105
- } else if (isPlainObject(item) || isArray(item)) {
106
- sanitizeBlankStrings(item,{})
107
- } else {
108
- if (item && typeof item === 'string') val[i] = item.replace(/[ \t]+$/, '')
109
- }
110
- }
111
- return
112
- }
113
-
114
- if (isPlainObject(val)) {
115
- for (const key of Object.keys(val)) {
116
- const v = val[key]
117
- if (isBlankString(v)) {
118
- if (original && !Object.keys(original).includes(key)) delete val[key]
119
- else val[key] = null
120
- } else if (isPlainObject(v) || isArray(v)) {
121
- let originalChild = (original && (isPlainObject(original[key]) || isArray(original[key]))) ? original[key] : {}
122
- sanitizeBlankStrings(v, originalChild)
123
- } else {
124
- if (v && typeof v === 'string') val[key] = v.replace(/[ \t]+$/, '')
125
- }
126
- }
127
- }
128
- }
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
-
195
- watch(formData, (newValue) => {
196
- sanitizeBlankStrings(newValue)
197
- emit('update:modelValue', newValue)
198
- }, { deep: true })
199
-
200
- watch(() => props.modelValue, (newValue) => {
201
- formData.value = isObject(newValue) ? newValue : {}
202
- }, { deep: true, immediate: true })
203
-
204
- function diffPaths(
205
- a: any,
206
- b: any,
207
- base: string[] = [],
208
- out: string[] = []
209
- ): string[] {
210
- const keys = new Set<string>([
211
- ...Object.keys(a ?? {}),
212
- ...Object.keys(b ?? {}),
213
- ])
214
-
215
- for (const k of keys) {
216
- const av = a?.[k]
217
- const bv = b?.[k]
218
- if (!av && !bv) continue // ignore when both are falsy
219
-
220
- const path = [...base, k]
221
-
222
- if (isPlainObject(av) && isPlainObject(bv)) {
223
- diffPaths(av, bv, path, out)
224
- continue
225
- }
226
-
227
- if (!isEqual(av, bv)) {
228
- out.push(path.join('.'))
229
- }
230
- }
231
-
232
- return out
233
- }
234
-
235
- const injectedClass = computed<Record<string, string>>(() => {
236
- const data = (formData.value ?? {}) as Record<string, any>
237
- const result: Record<string, string> = {}
238
-
239
- if (!props.dirtyOnCreate && !props.originalData) return result
240
-
241
- const original = (props.originalData ?? {}) as Record<string, any>
242
- const cls = props.dirtyClass || 'form-data-dirty'
243
-
244
- const paths = diffPaths(data, original)
245
- for (const p of paths) result[p] = cls
246
-
247
- return result
248
- })
249
-
250
- const formComponent = shallowRef()
251
-
252
- function buildFormComponent() {
253
- if (!trimmedTemplate.value) return
254
- const originalConsoleError = console.warn
255
- console.warn = (error: any) => { throw new Error(error) } // eslint-disable-line
256
- try {
257
- const componentTemplate = '<form-pad ref="formPadTemplate" v-model="formComponentData" :originalData="originalData" :disabled="disabled" :readonly="readonly" :decoration="decoration" :isolated="isolated"><template v-slot="{ data,isDisabled,isReadonly,rules,formProvided,decoration,injectedClass }">' + trimmedTemplate.value + '</template></form-pad>'
258
- compile(componentTemplate)
259
- formComponent.value = defineComponent({
260
- components: { FormPad },
261
- props: {
262
- modelValue: { type: Object, default: undefined },
263
- originalData: { type: Object, default: undefined },
264
- disabled: { type: Boolean, default: false },
265
- readonly: { type: Boolean, default: false },
266
- decoration: { type: Object, default: () => { return {} } },
267
- isolated: { type: Boolean, default: false },
268
- },
269
- emits: ['update:modelValue'],
270
- setup(props, ctx) {
271
- const formComponentData = ref<any>({})
272
- const formPadTemplate = ref<any>({})
273
- watch(formComponentData, (newValue) => {
274
- sanitizeBlankStrings(newValue)
275
- ctx.emit('update:modelValue', newValue)
276
- }, { deep: true })
277
- watch(() => props.modelValue, (newValue) => {
278
- formComponentData.value = isObject(newValue) ? newValue : {}
279
- }, { deep: true, immediate: true })
280
- const isValid = computed(() => formPadTemplate.value.isValid)
281
- return {
282
- formComponentData,
283
- formPadTemplate,
284
- reset: () => formPadTemplate.value.reset(),
285
- validate: () => formPadTemplate.value.validate(),
286
- resetValidate: () => formPadTemplate.value.resetValidate(),
287
- isValid,
288
- ...templateScriptFunction.value(props, ctx, ...Object.values(vueFunctions)),
289
- autoSanitizedDisplay
290
- }
291
- },
292
- template: componentTemplate,
293
- })
294
- }
295
- catch (e) {
296
- formComponent.value = null
297
- console.error(e)
298
- }
299
- console.warn = originalConsoleError
300
- }
301
-
302
- function reset() {
303
- if (!formInjected.value) formPad.value.reset()
304
- else formInjected.value.items.forEach((item: any) => item.reset())
305
- }
306
-
307
- function validate() {
308
- if (!formInjected.value) formPad.value.validate()
309
- else formInjected.value.items.forEach((item: any) => item.validate())
310
- }
311
-
312
- function resetValidate() {
313
- if (!formInjected.value) formPad.value.resetValidate()
314
- else formInjected.value.items.forEach((item: any) => item.resetValidate())
315
- }
316
-
317
- const isValid = computed(() => {
318
- validate()
319
- return formInjected.value ? formInjected.value.isValid || false : formPad.value.isValid || false
320
- })
321
-
322
- onMounted(() => {
323
- if (!props.isolated) formInjected.value = inject(formInjectKey, false)
324
- buildFormComponent()
325
- })
326
-
327
- watchDebounced(() => props.template, (newValue) => {
328
- trimmedTemplate.value = useDocumentTemplate(newValue,props.parentTemplates).trim() || ''
329
- buildFormComponent()
330
- }, { debounce: 500, maxWait: 5000, deep: true, immediate: true })
331
- watchDebounced(() => props.templateScript, buildFormComponent, { debounce: 500, maxWait: 5000 })
332
-
333
- defineExpose({
334
- isValid,
335
- disabled,
336
- readonly,
337
- reset,
338
- validate,
339
- resetValidate,
340
- })
341
- </script>
342
-
343
- <template>
344
- <v-form
345
- v-if="!formInjected && !trimmedTemplate"
346
- ref="formPad"
347
- :disabled="disabled"
348
- :readonly="readonly"
349
- :class="$attrs.class"
350
- autocomplete="off"
351
- >
352
- <template #default="formProvided">
353
- <slot
354
- :data="formData"
355
- :form-provided="formProvided"
356
- :is-disabled="disabled"
357
- :is-readonly="readonly"
358
- :rules="rules"
359
- :decoration="decoration"
360
- :injectedClass="injectedClass"
361
- />
362
- </template>
363
- </v-form>
364
- <template v-if="formInjected && !trimmedTemplate">
365
- <slot
366
- :data="formData"
367
- :form-provided="formInjected"
368
- :is-disabled="disabled"
369
- :is-readonly="readonly"
370
- :rules="rules"
371
- :decoration="decoration"
372
- :injectedClass="injectedClass"
373
- />
374
- </template>
375
- <component
376
- :is="formComponent"
377
- v-if="trimmedTemplate"
378
- ref="formPad"
379
- v-model="formData"
380
- :originalData="originalData"
381
- :disabled="disabled"
382
- :readonly="readonly"
383
- :decoration="decoration"
384
- :isolated="isolated"
385
- :class="$attrs.class"
386
- />
387
- </template>
1
+ <script lang="ts" setup>
2
+ /**
3
+ * FormPad is a schema-driven form field component that binds model data, renders field UI, and emits normalized updates.
4
+ * This doc block is consumed by vue-docgen for generated API documentation.
5
+ */
6
+ /* eslint-disable @typescript-eslint/no-explicit-any,import/no-self-import */
7
+ import {
8
+ compile,
9
+ computed,
10
+ defineComponent,
11
+ defineOptions,
12
+ inject,
13
+ onMounted,
14
+ ref,
15
+ shallowRef,
16
+ watch,
17
+ withDefaults
18
+ } from 'vue'
19
+ import {watchDebounced} from '@vueuse/core'
20
+ import {useRules} from '../../composables/utils/validation'
21
+ import {useDocumentTemplate} from '../../composables/document/template'
22
+ import { isObject, isArray, isString, isPlainObject, isEqual, debounce } from 'lodash-es'
23
+ import FormPad from './Pad.vue'
24
+
25
+ defineOptions({
26
+ inheritAttrs: false,
27
+ })
28
+
29
+ interface Props {
30
+ modelValue?: object // Bound value for v-model synchronization with the parent component.
31
+ originalData?: object // Original baseline data used for dirty-checking and reset behavior.
32
+ template?: any // Template object used to render dynamic form/pad structure.
33
+ templateScript?: string // Optional script expression used to post-process template behavior.
34
+ disabled?: boolean // disables user interaction for this field
35
+ readonly?: boolean // renders as read-only while keeping value visible
36
+ isolated?: boolean // boolean flag controlling runtime behavior
37
+ decoration?: object // Decoration config used to style or annotate generated fields.
38
+ parentTemplates?: string|string[] // Parent template code(s) used for template inheritance/extension.
39
+ dirtyClass?: string // CSS class applied when form state differs from original data.
40
+ dirtyOnCreate?: boolean // Marks new forms as dirty immediately on first render.
41
+ sanitizeDelay?: number // Debounce delay before sanitizing emitted form values.
42
+ }
43
+
44
+ /**
45
+ * Public props accepted by FormPad.
46
+ * Document each prop field with intent, defaults, and side effects for clear generated docs.
47
+ */
48
+ const props = withDefaults(defineProps<Props>(), {
49
+ disabled: false,
50
+ readonly: false,
51
+ isolated: false,
52
+ decoration: () => { return {} },
53
+ parentTemplates: (): string[] => [],
54
+ dirtyClass: "form-data-dirty",
55
+ dirtyOnCreate: false,
56
+ sanitizeDelay: 2000,
57
+ })
58
+
59
+ /**
60
+ * Custom events emitted by FormPad.
61
+ * Parents can listen to these events to react to user actions and internal state changes.
62
+ */
63
+ const emit = defineEmits(['update:modelValue'])
64
+
65
+ const disabled = ref(props.disabled)
66
+ const readonly = ref(props.readonly)
67
+ const decoration = ref(props.decoration)
68
+
69
+ watch(() => props.disabled, (newValue) => {
70
+ disabled.value = newValue
71
+ })
72
+
73
+ watch(() => props.readonly, (newValue) => {
74
+ readonly.value = newValue
75
+ })
76
+
77
+ watch(() => props.decoration, (newValue) => {
78
+ decoration.value = newValue
79
+ }, { deep: true })
80
+
81
+ const { rules } = useRules()
82
+
83
+ const trimmedTemplate = ref<string>('')
84
+
85
+ const vueFunctions = { ref, shallowRef, computed, watch, onMounted }
86
+
87
+ const templateScriptFunction = computed(() => {
88
+ let templateScript = props.templateScript?.trim() || 'return {}'
89
+ const pattern = /^\s*[{[].*[}\]]\s*$/
90
+ if (pattern.test(templateScript)) templateScript = 'return {}'
91
+ return Function('props', 'ctx', ...Object.keys(vueFunctions), templateScript)
92
+ })
93
+
94
+ const formPad = ref()
95
+ const formInjectKey = Symbol.for('vuetify:form')
96
+ const formInjected = ref()
97
+
98
+ const formData = ref<any>({})
99
+
100
+ function isBlankString(v: unknown): v is string {
101
+ return isString(v) && v.trim().length === 0
102
+ }
103
+
104
+ const sanitizeBlankStrings = debounce(sanitizeBlankStringsRaw, props.sanitizeDelay)
105
+
106
+ function sanitizeBlankStringsRaw(val: any, original?: any): void {
107
+ if (!original && props.originalData) {
108
+ sanitizeBlankStrings(val, props.originalData)
109
+ return
110
+ }
111
+
112
+ if (isArray(val)) {
113
+ for (let i = val.length - 1; i >= 0; i--) {
114
+ const item = val[i]
115
+ if (isBlankString(item)) {
116
+ val.splice(i, 1)
117
+ } else if (isPlainObject(item) || isArray(item)) {
118
+ sanitizeBlankStrings(item,{})
119
+ } else {
120
+ if (item && typeof item === 'string') val[i] = item.replace(/[ \t]+$/, '')
121
+ }
122
+ }
123
+ return
124
+ }
125
+
126
+ if (isPlainObject(val)) {
127
+ for (const key of Object.keys(val)) {
128
+ const v = val[key]
129
+ if (isBlankString(v)) {
130
+ if (original && !Object.keys(original).includes(key)) delete val[key]
131
+ else val[key] = null
132
+ } else if (isPlainObject(v) || isArray(v)) {
133
+ let originalChild = (original && (isPlainObject(original[key]) || isArray(original[key]))) ? original[key] : {}
134
+ sanitizeBlankStrings(v, originalChild)
135
+ } else {
136
+ if (v && typeof v === 'string') val[key] = v.replace(/[ \t]+$/, '')
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ function autoSanitizedDisplay(item: any, separator: string = ","): string | undefined {
143
+ const isEmptyScalar = (v: any) =>
144
+ v === null ||
145
+ v === undefined ||
146
+ (typeof v === "string" && v.trim() === "") ||
147
+ (typeof v === "number" && Number.isNaN(v));
148
+
149
+ const toStr = (v: any): string | undefined => {
150
+ if (isEmptyScalar(v)) return undefined;
151
+ if (typeof v === "string") return v.trim();
152
+ if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
153
+ if (typeof v === "symbol") return v.description ?? String(v);
154
+ if (typeof v === "function") return v.name ? `[Function ${v.name}]` : "[Function]";
155
+ return undefined;
156
+ };
157
+
158
+ // 1) empty -> undefined
159
+ if (isEmptyScalar(item)) return undefined;
160
+
161
+ // 2) array -> recurse + join
162
+ if (Array.isArray(item)) {
163
+ const parts = item
164
+ .map((x) => autoSanitizedDisplay(x, separator))
165
+ .filter((s): s is string => typeof s === "string" && s.trim() !== "");
166
+ return parts.length ? parts.join(separator) : undefined;
167
+ }
168
+
169
+ // simple scalars
170
+ const scalar = toStr(item);
171
+ if (scalar !== undefined) return scalar;
172
+
173
+ // 3) object
174
+ if (typeof item === "object") {
175
+ // 3.1 label
176
+ if ("label" in item) {
177
+ const v = autoSanitizedDisplay((item as any).label, separator) ?? toStr((item as any).label);
178
+ if (v !== undefined) return v;
179
+ }
180
+ // 3.2 value
181
+ if ("value" in item) {
182
+ const v = autoSanitizedDisplay((item as any).value, separator) ?? toStr((item as any).value);
183
+ if (v !== undefined) return v;
184
+ }
185
+
186
+ // 3.3 stringify attributes as key:value (recurse values)
187
+ const entries = Object.entries(item as Record<string, any>)
188
+ .map(([k, v]) => {
189
+ const rendered = autoSanitizedDisplay(v, separator) ?? toStr(v);
190
+ if (rendered === undefined || rendered.trim() === "") return undefined;
191
+ return `${k}:${rendered}`;
192
+ })
193
+ .filter((x): x is string => typeof x === "string" && x.trim() !== "");
194
+
195
+ return entries.length ? entries.join(separator) : undefined;
196
+ }
197
+
198
+ // fallback
199
+ try {
200
+ const s = String(item);
201
+ return s.trim() ? s : undefined;
202
+ } catch {
203
+ return undefined;
204
+ }
205
+ }
206
+
207
+ watch(formData, (newValue) => {
208
+ sanitizeBlankStrings(newValue)
209
+ emit('update:modelValue', newValue)
210
+ }, { deep: true })
211
+
212
+ watch(() => props.modelValue, (newValue) => {
213
+ formData.value = isObject(newValue) ? newValue : {}
214
+ }, { deep: true, immediate: true })
215
+
216
+ function diffPaths(
217
+ a: any,
218
+ b: any,
219
+ base: string[] = [],
220
+ out: string[] = []
221
+ ): string[] {
222
+ const keys = new Set<string>([
223
+ ...Object.keys(a ?? {}),
224
+ ...Object.keys(b ?? {}),
225
+ ])
226
+
227
+ for (const k of keys) {
228
+ const av = a?.[k]
229
+ const bv = b?.[k]
230
+ if (!av && !bv) continue // ignore when both are falsy
231
+
232
+ const path = [...base, k]
233
+
234
+ if (isPlainObject(av) && isPlainObject(bv)) {
235
+ diffPaths(av, bv, path, out)
236
+ continue
237
+ }
238
+
239
+ if (!isEqual(av, bv)) {
240
+ out.push(path.join('.'))
241
+ }
242
+ }
243
+
244
+ return out
245
+ }
246
+
247
+ const injectedClass = computed<Record<string, string>>(() => {
248
+ const data = (formData.value ?? {}) as Record<string, any>
249
+ const result: Record<string, string> = {}
250
+
251
+ if (!props.dirtyOnCreate && !props.originalData) return result
252
+
253
+ const original = (props.originalData ?? {}) as Record<string, any>
254
+ const cls = props.dirtyClass || 'form-data-dirty'
255
+
256
+ const paths = diffPaths(data, original)
257
+ for (const p of paths) result[p] = cls
258
+
259
+ return result
260
+ })
261
+
262
+ const formComponent = shallowRef()
263
+
264
+ function buildFormComponent() {
265
+ if (!trimmedTemplate.value) return
266
+ const originalConsoleError = console.warn
267
+ console.warn = (error: any) => { throw new Error(error) } // eslint-disable-line
268
+ try {
269
+ const componentTemplate = '<form-pad ref="formPadTemplate" v-model="formComponentData" :originalData="originalData" :disabled="disabled" :readonly="readonly" :decoration="decoration" :isolated="isolated"><template v-slot="{ data,isDisabled,isReadonly,rules,formProvided,decoration,injectedClass }">' + trimmedTemplate.value + '</template></form-pad>'
270
+ compile(componentTemplate)
271
+ formComponent.value = defineComponent({
272
+ components: { FormPad },
273
+ props: {
274
+ modelValue: { type: Object, default: undefined },
275
+ originalData: { type: Object, default: undefined },
276
+ disabled: { type: Boolean, default: false },
277
+ readonly: { type: Boolean, default: false },
278
+ decoration: { type: Object, default: () => { return {} } },
279
+ isolated: { type: Boolean, default: false },
280
+ },
281
+ emits: ['update:modelValue'],
282
+ setup(props, ctx) {
283
+ const formComponentData = ref<any>({})
284
+ const formPadTemplate = ref<any>({})
285
+ watch(formComponentData, (newValue) => {
286
+ sanitizeBlankStrings(newValue)
287
+ ctx.emit('update:modelValue', newValue)
288
+ }, { deep: true })
289
+ watch(() => props.modelValue, (newValue) => {
290
+ formComponentData.value = isObject(newValue) ? newValue : {}
291
+ }, { deep: true, immediate: true })
292
+ const isValid = computed(() => formPadTemplate.value.isValid)
293
+ return {
294
+ formComponentData,
295
+ formPadTemplate,
296
+ reset: () => formPadTemplate.value.reset(),
297
+ validate: () => formPadTemplate.value.validate(),
298
+ resetValidate: () => formPadTemplate.value.resetValidate(),
299
+ isValid,
300
+ ...templateScriptFunction.value(props, ctx, ...Object.values(vueFunctions)),
301
+ autoSanitizedDisplay
302
+ }
303
+ },
304
+ template: componentTemplate,
305
+ })
306
+ }
307
+ catch (e) {
308
+ formComponent.value = null
309
+ console.error(e)
310
+ }
311
+ console.warn = originalConsoleError
312
+ }
313
+
314
+ function reset() {
315
+ if (!formInjected.value) formPad.value.reset()
316
+ else formInjected.value.items.forEach((item: any) => item.reset())
317
+ }
318
+
319
+ function validate() {
320
+ if (!formInjected.value) formPad.value.validate()
321
+ else formInjected.value.items.forEach((item: any) => item.validate())
322
+ }
323
+
324
+ function resetValidate() {
325
+ if (!formInjected.value) formPad.value.resetValidate()
326
+ else formInjected.value.items.forEach((item: any) => item.resetValidate())
327
+ }
328
+
329
+ const isValid = computed(() => {
330
+ validate()
331
+ return formInjected.value ? formInjected.value.isValid || false : formPad.value.isValid || false
332
+ })
333
+
334
+ onMounted(() => {
335
+ if (!props.isolated) formInjected.value = inject(formInjectKey, false)
336
+ buildFormComponent()
337
+ })
338
+
339
+ watchDebounced(() => props.template, (newValue) => {
340
+ trimmedTemplate.value = useDocumentTemplate(newValue,props.parentTemplates).trim() || ''
341
+ buildFormComponent()
342
+ }, { debounce: 500, maxWait: 5000, deep: true, immediate: true })
343
+ watchDebounced(() => props.templateScript, buildFormComponent, { debounce: 500, maxWait: 5000 })
344
+
345
+ defineExpose({
346
+ isValid,
347
+ disabled,
348
+ readonly,
349
+ reset,
350
+ validate,
351
+ resetValidate,
352
+ })
353
+ </script>
354
+
355
+ <template>
356
+ <v-form
357
+ v-if="!formInjected && !trimmedTemplate"
358
+ ref="formPad"
359
+ :disabled="disabled"
360
+ :readonly="readonly"
361
+ :class="$attrs.class"
362
+ autocomplete="off"
363
+ >
364
+ <template #default="formProvided">
365
+ <slot
366
+ :data="formData"
367
+ :form-provided="formProvided"
368
+ :is-disabled="disabled"
369
+ :is-readonly="readonly"
370
+ :rules="rules"
371
+ :decoration="decoration"
372
+ :injectedClass="injectedClass"
373
+ />
374
+ </template>
375
+ </v-form>
376
+ <template v-if="formInjected && !trimmedTemplate">
377
+ <slot
378
+ :data="formData"
379
+ :form-provided="formInjected"
380
+ :is-disabled="disabled"
381
+ :is-readonly="readonly"
382
+ :rules="rules"
383
+ :decoration="decoration"
384
+ :injectedClass="injectedClass"
385
+ />
386
+ </template>
387
+ <component
388
+ :is="formComponent"
389
+ v-if="trimmedTemplate"
390
+ ref="formPad"
391
+ v-model="formData"
392
+ :originalData="originalData"
393
+ :disabled="disabled"
394
+ :readonly="readonly"
395
+ :decoration="decoration"
396
+ :isolated="isolated"
397
+ :class="$attrs.class"
398
+ />
399
+ </template>
388
400
  <style>
389
401
  .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
402
  </style>