@ramathibodi/nuxt-commons 0.1.74 → 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 (96) hide show
  1. package/README.md +115 -115
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Alert.vue +58 -58
  4. package/dist/runtime/components/BarcodeReader.vue +130 -130
  5. package/dist/runtime/components/ExportCSV.vue +110 -110
  6. package/dist/runtime/components/FileBtn.vue +79 -79
  7. package/dist/runtime/components/ImportCSV.vue +151 -151
  8. package/dist/runtime/components/MrzReader.vue +168 -168
  9. package/dist/runtime/components/SplitterPanel.vue +67 -67
  10. package/dist/runtime/components/TabsGroup.vue +39 -39
  11. package/dist/runtime/components/TextBarcode.vue +66 -66
  12. package/dist/runtime/components/device/IdCardButton.vue +95 -95
  13. package/dist/runtime/components/device/IdCardWebSocket.vue +207 -207
  14. package/dist/runtime/components/device/Scanner.vue +350 -350
  15. package/dist/runtime/components/dialog/Confirm.vue +112 -112
  16. package/dist/runtime/components/dialog/Host.vue +88 -88
  17. package/dist/runtime/components/dialog/Index.vue +84 -84
  18. package/dist/runtime/components/dialog/Loading.vue +51 -51
  19. package/dist/runtime/components/dialog/default/Confirm.vue +112 -112
  20. package/dist/runtime/components/dialog/default/Loading.vue +60 -60
  21. package/dist/runtime/components/dialog/default/Notify.vue +82 -82
  22. package/dist/runtime/components/dialog/default/Printing.vue +46 -46
  23. package/dist/runtime/components/dialog/default/VerifyUser.vue +144 -144
  24. package/dist/runtime/components/document/Form.vue +50 -50
  25. package/dist/runtime/components/document/TemplateBuilder.vue +536 -536
  26. package/dist/runtime/components/form/ActionPad.vue +156 -156
  27. package/dist/runtime/components/form/Birthdate.vue +116 -116
  28. package/dist/runtime/components/form/CheckboxGroup.vue +99 -99
  29. package/dist/runtime/components/form/CodeEditor.vue +45 -45
  30. package/dist/runtime/components/form/Date.vue +270 -270
  31. package/dist/runtime/components/form/DateTime.vue +220 -220
  32. package/dist/runtime/components/form/Dialog.vue +178 -178
  33. package/dist/runtime/components/form/EditPad.vue +157 -157
  34. package/dist/runtime/components/form/File.vue +295 -295
  35. package/dist/runtime/components/form/Hidden.vue +44 -44
  36. package/dist/runtime/components/form/Iterator.vue +538 -538
  37. package/dist/runtime/components/form/Login.vue +143 -143
  38. package/dist/runtime/components/form/Pad.vue +399 -399
  39. package/dist/runtime/components/form/SignPad.vue +226 -226
  40. package/dist/runtime/components/form/System.vue +34 -34
  41. package/dist/runtime/components/form/Table.vue +391 -391
  42. package/dist/runtime/components/form/TableData.vue +236 -236
  43. package/dist/runtime/components/form/Time.vue +177 -177
  44. package/dist/runtime/components/form/images/Capture.vue +245 -245
  45. package/dist/runtime/components/form/images/Edit.vue +133 -133
  46. package/dist/runtime/components/form/images/Field.vue +331 -331
  47. package/dist/runtime/components/form/images/Pad.vue +54 -54
  48. package/dist/runtime/components/label/Date.vue +37 -37
  49. package/dist/runtime/components/label/DateAgo.vue +102 -102
  50. package/dist/runtime/components/label/DateCount.vue +152 -152
  51. package/dist/runtime/components/label/Field.vue +111 -111
  52. package/dist/runtime/components/label/FormatMoney.vue +37 -37
  53. package/dist/runtime/components/label/Mask.vue +46 -46
  54. package/dist/runtime/components/label/Object.vue +21 -21
  55. package/dist/runtime/components/master/Autocomplete.vue +89 -89
  56. package/dist/runtime/components/master/Combobox.vue +88 -88
  57. package/dist/runtime/components/master/RadioGroup.vue +90 -90
  58. package/dist/runtime/components/master/Select.vue +70 -70
  59. package/dist/runtime/components/master/label.vue +55 -55
  60. package/dist/runtime/components/model/Autocomplete.vue +91 -91
  61. package/dist/runtime/components/model/Combobox.vue +90 -90
  62. package/dist/runtime/components/model/Pad.vue +114 -114
  63. package/dist/runtime/components/model/Select.vue +78 -84
  64. package/dist/runtime/components/model/Table.vue +370 -370
  65. package/dist/runtime/components/model/iterator.vue +497 -497
  66. package/dist/runtime/components/model/label.vue +58 -58
  67. package/dist/runtime/components/pdf/Print.vue +75 -75
  68. package/dist/runtime/components/pdf/View.vue +146 -146
  69. package/dist/runtime/composables/dialog.d.ts +1 -1
  70. package/dist/runtime/composables/graphql.d.ts +1 -1
  71. package/dist/runtime/composables/graphqlModel.d.ts +9 -9
  72. package/dist/runtime/composables/graphqlModelItem.d.ts +7 -7
  73. package/dist/runtime/composables/graphqlModelOperation.d.ts +6 -6
  74. package/dist/runtime/composables/userPermission.d.ts +1 -1
  75. package/dist/runtime/labs/Calendar.vue +99 -99
  76. package/dist/runtime/labs/form/EditMobile.vue +152 -152
  77. package/dist/runtime/labs/form/TextFieldMask.vue +43 -43
  78. package/dist/runtime/plugins/clientConfig.d.ts +1 -1
  79. package/dist/runtime/plugins/default.d.ts +1 -1
  80. package/dist/runtime/plugins/dialogManager.d.ts +1 -1
  81. package/dist/runtime/plugins/permission.d.ts +1 -1
  82. package/dist/runtime/types/alert.d.ts +11 -11
  83. package/dist/runtime/types/clientConfig.d.ts +13 -13
  84. package/dist/runtime/types/dialogManager.d.ts +35 -35
  85. package/dist/runtime/types/formDialog.d.ts +5 -5
  86. package/dist/runtime/types/graphqlOperation.d.ts +23 -23
  87. package/dist/runtime/types/menu.d.ts +31 -31
  88. package/dist/runtime/types/modules.d.ts +7 -7
  89. package/dist/runtime/types/permission.d.ts +13 -13
  90. package/package.json +131 -131
  91. package/scripts/enrich-vue-docs-from-ai.mjs +197 -197
  92. package/scripts/generate-ai-summary.mjs +321 -321
  93. package/scripts/generate-composables-md.mjs +129 -129
  94. package/scripts/postInstall.cjs +70 -70
  95. package/templates/.codegen/codegen.ts +32 -32
  96. package/templates/.codegen/plugin-schema-object.js +161 -161
@@ -1,402 +1,402 @@
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>
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>
400
400
  <style>
401
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}
402
402
  </style>