@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,538 +1,538 @@
1
- <script lang="ts" setup>
2
- /**
3
- * FormIterator 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
- import {computed, defineExpose, defineOptions, nextTick, ref, useAttrs, useSlots, useTemplateRef, watch} from 'vue'
7
- import {omit} from 'lodash-es'
8
- import type {FormDialogCallback} from '../../types/formDialog'
9
- import {VDataIterator} from "vuetify/components/VDataIterator";
10
- import {VDataTable} from "vuetify/components/VDataTable";
11
- import {VInput} from 'vuetify/components/VInput'
12
- import {useDisplay} from 'vuetify'
13
-
14
- defineOptions({
15
- inheritAttrs: false,
16
- })
17
-
18
- interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$props']>,/* @vue-ignore */ InstanceType<typeof VDataTable['$props']> {
19
- title: string // Toolbar title shown above iterator/table view.
20
- noDataText?: string // Message shown when no rows are available to render.
21
- modelValue?: Record<string, any>[] // Parent-provided rows; component emits reordered/edited rows back via v-model.
22
- modelKey?: string // Row identity key used for edit/delete/move operations and table item-value.
23
- dialogFullscreen?: boolean // Opens item editor dialog in fullscreen mode by default.
24
- initialData?: Record<string, any> // Default payload merged into a new item before editing.
25
- toolbarColor?: string // Toolbar/action color theme token.
26
- importable?: boolean // Shows import action and allows creating rows from imported records.
27
- exportable?: boolean // Shows export action for the current iterator dataset.
28
- insertable?: boolean // Enables add/create action for new rows.
29
- searchable?: boolean // Shows search control and applies client-side filtering keyword.
30
-
31
- loading?: boolean // External loading state passed through to iterator/table rendering.
32
-
33
- viewSwitch?: boolean // Enables UI control that lets users switch between card iterator and table view.
34
- viewSwitchMultiple?: boolean // Allows multi-select behavior in view switch control when enabled.
35
-
36
- cols?: string | number | boolean // Base grid columns for iterator card items.
37
- xxl?: string | number | boolean // Card grid columns at `xxl` breakpoint.
38
- xl?: string | number | boolean // Card grid columns at `xl` breakpoint.
39
- lg?: string | number | boolean // Card grid columns at `lg` breakpoint.
40
- md?: string | number | boolean // Card grid columns at `md` breakpoint.
41
- sm?: string | number | boolean // Card grid columns at `sm` breakpoint.
42
- itemsPerPage?: string | number // Page size for iterator/table; supports `'all'` semantics.
43
-
44
- preferTable?: string | number | boolean // Global auto-switch rule: `true` always table, number means table when item count reaches threshold.
45
- preferTableXxl?: string | number | boolean // Breakpoint-specific override for `preferTable` at `xxl`.
46
- preferTableXl?: string | number | boolean // Breakpoint-specific override for `preferTable` at `xl`.
47
- preferTableLg?: string | number | boolean // Breakpoint-specific override for `preferTable` at `lg`.
48
- preferTableMd?: string | number | boolean // Breakpoint-specific override for `preferTable` at `md`.
49
- preferTableSm?: string | number | boolean // Breakpoint-specific override for `preferTable` at `sm`.
50
- }
51
-
52
- /**
53
- * Public props accepted by FormIterator.
54
- * Document each prop field with intent, defaults, and side effects for clear generated docs.
55
- */
56
- const props = withDefaults(defineProps<Props>(), {
57
- noDataText: 'ไม่พบข้อมูล',
58
- dialogFullscreen: false,
59
- modelKey: 'id',
60
- toolbarColor: 'primary',
61
- importable: true,
62
- exportable: true,
63
- insertable: true,
64
- searchable: true,
65
-
66
- loading: false,
67
-
68
- viewSwitch: false,
69
- viewSwitchMultiple:false,
70
-
71
- cols: 12,
72
- xxl: false,
73
- xl: false,
74
- lg: 2,
75
- md: 4,
76
- sm: 6,
77
- itemsPerPage: 12,
78
- })
79
-
80
- /**
81
- * Custom events emitted by FormIterator.
82
- * Parents can listen to these events to react to user actions and internal state changes.
83
- */
84
- const emit = defineEmits(['update:modelValue'])
85
-
86
- const attrs = useAttrs()
87
- const plainAttrs = computed(() => {
88
- return omit(attrs, ['modelValue', 'onUpdate:modelValue'])
89
- })
90
-
91
- const inputRef = useTemplateRef<VInput>("inputRef")
92
-
93
- const slots = useSlots()
94
- const tableSlots = computed(() => {
95
- return omit(slots, ['item'])
96
- })
97
-
98
- const display = useDisplay()
99
- const isProgrammaticSet = ref(false)
100
- const userOverrodeView = ref(false)
101
-
102
- const viewType = ref<string[] | string>('iterator')
103
-
104
- function setViewTypeSafely(val: 'iterator' | 'table') {
105
- isProgrammaticSet.value = true
106
- if (Array.isArray(viewType.value)) {
107
- viewType.value = [val]
108
- } else {
109
- viewType.value = val
110
- }
111
- nextTick(() => { isProgrammaticSet.value = false })
112
- }
113
-
114
- watch(viewType, () => {
115
- if (!isProgrammaticSet.value) userOverrodeView.value = true
116
- })
117
-
118
- function parsePrefer(val: unknown): boolean | number {
119
- if (val === true) return true
120
- const n = Number(val)
121
- return Number.isFinite(n) && n > 0 ? n : false
122
- }
123
-
124
- const computedPreferTable = computed<boolean | number>(() => {
125
- const bp = display.name?.value
126
- if (bp === 'xxl' && props.preferTableXxl !== undefined) return parsePrefer(props.preferTableXxl)
127
- if (bp === 'xl' && props.preferTableXl !== undefined) return parsePrefer(props.preferTableXl)
128
- if (bp === 'lg' && props.preferTableLg !== undefined) return parsePrefer(props.preferTableLg)
129
- if (bp === 'md' && props.preferTableMd !== undefined) return parsePrefer(props.preferTableMd)
130
- if (bp === 'sm' && props.preferTableSm !== undefined) return parsePrefer(props.preferTableSm)
131
- return parsePrefer(props.preferTable)
132
- })
133
-
134
- const items = ref<Record<string, any>[]>([])
135
- const search = ref<string>()
136
- const currentItem = ref<Record<string, any> | undefined>(undefined)
137
-
138
- function setSearch(keyword: string) {
139
- search.value = keyword
140
- }
141
-
142
- const isDialogOpen = ref<boolean>(false)
143
-
144
- watch(() => props.modelValue, (newValue) => {
145
- if (!Array.isArray(newValue) || !newValue.every(item => typeof item === 'object')) {
146
- items.value = []
147
- }
148
- else {
149
- let maxKey = 0
150
-
151
- newValue.forEach((item) => {
152
- if (!item.hasOwnProperty(props.modelKey)) {
153
- maxKey = Math.max(maxKey, ...newValue.map(i => i[props.modelKey] || 0))
154
- item[props.modelKey] = maxKey + 1
155
- }
156
- })
157
-
158
- items.value = newValue
159
- }
160
- }, { immediate: true })
161
-
162
- watch(items, (newValue) => {
163
- emit('update:modelValue', newValue)
164
- }, { deep: true })
165
-
166
- watch(
167
- [() => items.value?.length, computedPreferTable, () => display.name?.value],
168
- ([len, prefer]) => {
169
- if (userOverrodeView.value) return // respect explicit user choice forever (until remount)
170
-
171
- let target: 'iterator' | 'table' = 'iterator'
172
- if (prefer === true) {
173
- target = 'table'
174
- } else if (typeof prefer === 'number') {
175
- if (Number(len) >= prefer) target = 'table'
176
- }
177
-
178
- if (!viewType.value?.includes(target)) setViewTypeSafely(target)
179
- },
180
- { immediate: true }
181
- )
182
-
183
- const itemsPerPageInternal = ref<string | number>()
184
- watch(() => props.itemsPerPage, (newValue) => {
185
- if (newValue.toString().toLowerCase() == 'all') itemsPerPageInternal.value = '-1'
186
- else if (newValue) itemsPerPageInternal.value = newValue
187
- }, { immediate: true })
188
-
189
- function createItem(item: Record<string, any>, callback?: FormDialogCallback) {
190
- if (items.value.length > 0) item[props.modelKey] = Math.max(...items.value.map(i => i[props.modelKey] || 0)) + 1
191
- else item[props.modelKey] = 1
192
-
193
- items.value.push(item)
194
-
195
- if (callback) callback.done()
196
- }
197
-
198
- function importItems(importItems: Record<string, any>[], callback?: FormDialogCallback) {
199
- importItems.forEach((item) => {
200
- createItem(item)
201
- })
202
- if (callback) callback.done()
203
- }
204
-
205
- function updateItem(newItem: Record<string, any>, callback?: FormDialogCallback) {
206
- const index = items.value.findIndex(item => item[props.modelKey] === newItem[props.modelKey])
207
-
208
- if (index !== -1) {
209
- items.value[index] = newItem
210
- }
211
-
212
- if (callback) callback.done()
213
- }
214
-
215
- function moveUpItem(currentItem: Record<string, any>, callback?: FormDialogCallback) {
216
- const index = items.value.findIndex(item => item[props.modelKey] === currentItem[props.modelKey])
217
-
218
- if (index > 0) {
219
- const temp = items.value[index - 1]
220
- items.value[index - 1] = items.value[index]
221
- items.value[index] = temp
222
- }
223
-
224
- if (callback) callback.done()
225
- }
226
-
227
- function moveDownItem(currentItem: Record<string, any>, callback?: FormDialogCallback) {
228
- const index = items.value.findIndex(item => item[props.modelKey] === currentItem[props.modelKey])
229
-
230
- if (index >= 0 && index < items.value.length - 1) {
231
- const temp = items.value[index + 1]
232
- items.value[index + 1] = items.value[index]
233
- items.value[index] = temp
234
- }
235
-
236
- if (callback) callback.done()
237
- }
238
-
239
- function moveToItem(currentItem: Record<string, any>, callback?: FormDialogCallback) {
240
- const index = items.value.findIndex(item => item[props.modelKey] === currentItem[props.modelKey]);
241
-
242
- if (index !== -1) {
243
- const newPosition = prompt("Enter the new position (0-based index):");
244
- const parsedPosition = parseInt(<string>newPosition, 10);
245
-
246
- if (isNaN(parsedPosition) || parsedPosition < 0 || parsedPosition >= items.value.length) {
247
- alert("Invalid position entered. Please enter a number between 0 and " + (items.value.length - 1));
248
- return
249
- }
250
-
251
- const [temp] = items.value.splice(index, 1);
252
-
253
- items.value.splice(parsedPosition, 0, temp);
254
- }
255
-
256
- if (callback) callback.done();
257
- }
258
-
259
- function deleteItem(deleteItem: Record<string, any>, callback?: FormDialogCallback) {
260
- const index = items.value.findIndex(item => item[props.modelKey] === deleteItem[props.modelKey])
261
-
262
- if (index !== -1) {
263
- items.value.splice(index, 1)
264
- }
265
-
266
- if (callback) callback.done()
267
- }
268
-
269
- function openDialog(item?: object) {
270
- currentItem.value = item
271
- nextTick(() => {
272
- isDialogOpen.value = true
273
- })
274
- }
275
-
276
- const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpItem, moveDownItem,moveToItem,setSearch })
277
-
278
- const isValid = computed(()=>{
279
- return inputRef.value?.isValid
280
- })
281
-
282
- const errorMessages = computed(()=>{
283
- return inputRef.value?.errorMessages
284
- })
285
-
286
- defineExpose({
287
- errorMessages,
288
- isValid,
289
- reset: ()=>inputRef.value?.reset(),
290
- resetValidation : ()=>inputRef.value?.resetValidation(),
291
- validate : ()=>inputRef.value?.validate(),
292
- operation
293
- })
294
- </script>
295
-
296
- <template>
297
- <v-input v-model="items" v-bind="plainAttrs" ref="inputRef">
298
- <template #default="{isReadonly,isDisabled}">
299
- <v-container fluid class="ma-0 pa-0">
300
- <v-card>
301
- <v-data-iterator
302
- v-bind="plainAttrs"
303
- v-model:items-per-page="itemsPerPageInternal"
304
- :items="items"
305
- :item-value="modelKey"
306
- :search="search"
307
- :loading="loading"
308
- >
309
- <template #default="defaultProps" v-if="viewType.includes('iterator')">
310
- <slot
311
- v-bind="defaultProps"
312
- :operation="operation"
313
- >
314
- <v-container fluid>
315
- <v-row>
316
- <v-col
317
- v-for="(item, index) in defaultProps.items"
318
- :key="index"
319
- :cols="cols"
320
- :sm="sm"
321
- :md="md"
322
- :lg="lg"
323
- :xl="xl"
324
- :xxl="xxl"
325
- >
326
- <slot
327
- name="item"
328
- :item="item"
329
- :operation="operation"
330
- />
331
- </v-col>
332
- </v-row>
333
- </v-container>
334
- </slot>
335
- </template>
336
- <template #loader="loaderProps">
337
- <slot
338
- name="loader"
339
- v-bind="loaderProps"
340
- >
341
- <v-container fluid>
342
- <v-row>
343
- <v-col
344
- v-for="key in itemsPerPage"
345
- :key="key"
346
- :cols="cols"
347
- :sm="sm"
348
- :md="md"
349
- :lg="lg"
350
- :xl="xl"
351
- :xxl="xxl"
352
- >
353
- <slot name="loaderItem">
354
- <v-skeleton-loader
355
- type="paragraph"
356
- />
357
- </slot>
358
- </v-col>
359
- </v-row>
360
- </v-container>
361
- </slot>
362
- </template>
363
- <template #header="headerProps">
364
- <slot
365
- name="header"
366
- v-bind="headerProps"
367
- :items="items"
368
- :operation="operation"
369
- >
370
- <VToolbar :color="toolbarColor">
371
- <v-row
372
- justify="end"
373
- class="ma-1"
374
- dense
375
- no-gutters
376
- align="center"
377
- >
378
- <v-col cols="7">
379
- <VToolbarTitle class="pl-3">
380
- <slot name="title">
381
- {{ title }}
382
- </slot>
383
- </VToolbarTitle>
384
- </v-col>
385
- <v-col cols="5">
386
- <slot name="search" :items="items" :operation="operation" v-if="props.searchable">
387
- <VTextField
388
- v-model="search"
389
- class="justify-end w-100"
390
- density="compact"
391
- hide-details
392
- placeholder="ค้นหา"
393
- clearable
394
- variant="solo"
395
- />
396
- </slot>
397
- </v-col>
398
- </v-row>
399
-
400
- <VToolbarItems>
401
- <slot name="toolbarItems" :items="items" :operation="operation"/>
402
- <ImportCSV
403
- v-if="props.importable && !(isReadonly.value || isDisabled.value)"
404
- icon="mdi:mdi-file-upload"
405
- variant="flat"
406
- @import="importItems"
407
- :color="toolbarColor"
408
- />
409
- <ExportCSV
410
- v-if="props.exportable && items.length && !(isReadonly.value || isDisabled.value)"
411
- icon="mdi:mdi-file-download"
412
- variant="flat"
413
- :file-name="title"
414
- :model-value="items"
415
- :color="toolbarColor"
416
- />
417
- <VBtn
418
- v-if="props.insertable && !(isReadonly.value || isDisabled.value)"
419
- :color="toolbarColor"
420
- prepend-icon="mdi:mdi-plus"
421
- variant="flat"
422
- @click="openDialog()"
423
- >
424
- add
425
- </VBtn>
426
- <v-row align="center" class="mr-2 ml-2" v-if="viewSwitch">
427
- <v-btn-toggle v-model="viewType" :multiple="viewSwitchMultiple" mandatory>
428
- <v-btn icon="mdi mdi-view-grid-outline" value="iterator"></v-btn>
429
- <v-btn icon="mdi mdi-table-large" value="table"></v-btn>
430
- </v-btn-toggle>
431
- </v-row>
432
- </VToolbarItems>
433
- </VToolbar>
434
- </slot>
435
- <v-data-table
436
- v-bind="plainAttrs"
437
- color="primary"
438
- :items="items"
439
- :search="search"
440
- :loading="loading"
441
- v-if="viewType.includes('table')"
442
- >
443
- <!-- @ts-ignore -->
444
- <template
445
- v-for="(_, name, index) in (tableSlots as {})"
446
- :key="index"
447
- #[name]="slotData"
448
- >
449
- <slot
450
- :name="name"
451
- v-bind="((slotData || {}) as object)"
452
- :operation="operation"
453
- :isReadonly="isReadonly"
454
- :isDisabled="isDisabled"
455
- />
456
- </template>
457
- <template
458
- v-if="!$slots['item.operation'] && !(isReadonly.value || isDisabled.value)"
459
- #item.operation="props"
460
- >
461
- <v-icon density="compact" :disabled="props.index==0" @click="moveUpItem(props.item)">mdi:mdi-arrow-up-thick</v-icon>
462
- <v-icon density="compact" @click="moveToItem(props.item)">fa:fas fa-arrow-right-to-bracket</v-icon>
463
- <v-icon density="compact" :disabled="props.index==items.length-1" @click="moveDownItem(props.item)">mdi:mdi-arrow-down-thick</v-icon>
464
- </template>
465
- <template
466
- v-if="!$slots['item.action'] && !(isReadonly.value || isDisabled.value)"
467
- #item.action="{ item }"
468
- >
469
- <v-btn
470
- variant="flat"
471
- density="compact"
472
- icon="mdi:mdi-note-edit"
473
- @click="openDialog(item)"
474
- />
475
- <v-btn
476
- variant="flat"
477
- density="compact"
478
- icon="mdi:mdi-delete"
479
- @click="deleteItem(item)"
480
- />
481
- </template>
482
- </v-data-table>
483
- </template>
484
- <template #footer="footerProps" v-if="viewType.includes('iterator')">
485
- <v-container fluid>
486
- <v-row
487
- align-content="center"
488
- justify="end"
489
- dense
490
- >
491
- <v-spacer />
492
- <v-select
493
- v-model="itemsPerPageInternal"
494
- density="compact"
495
- variant="outlined"
496
- :items="[6, 12, 18, 24, 30, { title: 'All', value: '-1' }]"
497
- min-width="220"
498
- max-width="220"
499
- hide-details
500
- >
501
- <template #prepend>
502
- Items per page:
503
- </template>
504
- </v-select>
505
- <v-pagination
506
- density="compact"
507
- :length="footerProps.pageCount"
508
- total-visible="6"
509
- show-first-last-page
510
- @first="footerProps.setPage(1)"
511
- @last="footerProps.setPage(footerProps.pageCount)"
512
- @update:model-value="footerProps.setPage"
513
- />
514
- </v-row>
515
- </v-container>
516
- </template >
517
- </v-data-iterator>
518
- <FormDialog
519
- v-model="isDialogOpen"
520
- :title="title"
521
- :fullscreen="dialogFullscreen"
522
- :initial-data="initialData"
523
- :form-data="currentItem"
524
- @create="createItem"
525
- @update="updateItem"
526
- >
527
- <template #default="slotData">
528
- <slot
529
- name="form"
530
- v-bind="slotData"
531
- />
532
- </template>
533
- </FormDialog>
534
- </v-card>
535
- </v-container>
536
- </template>
537
- </v-input>
538
- </template>
1
+ <script lang="ts" setup>
2
+ /**
3
+ * FormIterator 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
+ import {computed, defineExpose, defineOptions, nextTick, ref, useAttrs, useSlots, useTemplateRef, watch} from 'vue'
7
+ import {omit} from 'lodash-es'
8
+ import type {FormDialogCallback} from '../../types/formDialog'
9
+ import {VDataIterator} from "vuetify/components/VDataIterator";
10
+ import {VDataTable} from "vuetify/components/VDataTable";
11
+ import {VInput} from 'vuetify/components/VInput'
12
+ import {useDisplay} from 'vuetify'
13
+
14
+ defineOptions({
15
+ inheritAttrs: false,
16
+ })
17
+
18
+ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$props']>,/* @vue-ignore */ InstanceType<typeof VDataTable['$props']> {
19
+ title: string // Toolbar title shown above iterator/table view.
20
+ noDataText?: string // Message shown when no rows are available to render.
21
+ modelValue?: Record<string, any>[] // Parent-provided rows; component emits reordered/edited rows back via v-model.
22
+ modelKey?: string // Row identity key used for edit/delete/move operations and table item-value.
23
+ dialogFullscreen?: boolean // Opens item editor dialog in fullscreen mode by default.
24
+ initialData?: Record<string, any> // Default payload merged into a new item before editing.
25
+ toolbarColor?: string // Toolbar/action color theme token.
26
+ importable?: boolean // Shows import action and allows creating rows from imported records.
27
+ exportable?: boolean // Shows export action for the current iterator dataset.
28
+ insertable?: boolean // Enables add/create action for new rows.
29
+ searchable?: boolean // Shows search control and applies client-side filtering keyword.
30
+
31
+ loading?: boolean // External loading state passed through to iterator/table rendering.
32
+
33
+ viewSwitch?: boolean // Enables UI control that lets users switch between card iterator and table view.
34
+ viewSwitchMultiple?: boolean // Allows multi-select behavior in view switch control when enabled.
35
+
36
+ cols?: string | number | boolean // Base grid columns for iterator card items.
37
+ xxl?: string | number | boolean // Card grid columns at `xxl` breakpoint.
38
+ xl?: string | number | boolean // Card grid columns at `xl` breakpoint.
39
+ lg?: string | number | boolean // Card grid columns at `lg` breakpoint.
40
+ md?: string | number | boolean // Card grid columns at `md` breakpoint.
41
+ sm?: string | number | boolean // Card grid columns at `sm` breakpoint.
42
+ itemsPerPage?: string | number // Page size for iterator/table; supports `'all'` semantics.
43
+
44
+ preferTable?: string | number | boolean // Global auto-switch rule: `true` always table, number means table when item count reaches threshold.
45
+ preferTableXxl?: string | number | boolean // Breakpoint-specific override for `preferTable` at `xxl`.
46
+ preferTableXl?: string | number | boolean // Breakpoint-specific override for `preferTable` at `xl`.
47
+ preferTableLg?: string | number | boolean // Breakpoint-specific override for `preferTable` at `lg`.
48
+ preferTableMd?: string | number | boolean // Breakpoint-specific override for `preferTable` at `md`.
49
+ preferTableSm?: string | number | boolean // Breakpoint-specific override for `preferTable` at `sm`.
50
+ }
51
+
52
+ /**
53
+ * Public props accepted by FormIterator.
54
+ * Document each prop field with intent, defaults, and side effects for clear generated docs.
55
+ */
56
+ const props = withDefaults(defineProps<Props>(), {
57
+ noDataText: 'ไม่พบข้อมูล',
58
+ dialogFullscreen: false,
59
+ modelKey: 'id',
60
+ toolbarColor: 'primary',
61
+ importable: true,
62
+ exportable: true,
63
+ insertable: true,
64
+ searchable: true,
65
+
66
+ loading: false,
67
+
68
+ viewSwitch: false,
69
+ viewSwitchMultiple:false,
70
+
71
+ cols: 12,
72
+ xxl: false,
73
+ xl: false,
74
+ lg: 2,
75
+ md: 4,
76
+ sm: 6,
77
+ itemsPerPage: 12,
78
+ })
79
+
80
+ /**
81
+ * Custom events emitted by FormIterator.
82
+ * Parents can listen to these events to react to user actions and internal state changes.
83
+ */
84
+ const emit = defineEmits(['update:modelValue'])
85
+
86
+ const attrs = useAttrs()
87
+ const plainAttrs = computed(() => {
88
+ return omit(attrs, ['modelValue', 'onUpdate:modelValue'])
89
+ })
90
+
91
+ const inputRef = useTemplateRef<VInput>("inputRef")
92
+
93
+ const slots = useSlots()
94
+ const tableSlots = computed(() => {
95
+ return omit(slots, ['item'])
96
+ })
97
+
98
+ const display = useDisplay()
99
+ const isProgrammaticSet = ref(false)
100
+ const userOverrodeView = ref(false)
101
+
102
+ const viewType = ref<string[] | string>('iterator')
103
+
104
+ function setViewTypeSafely(val: 'iterator' | 'table') {
105
+ isProgrammaticSet.value = true
106
+ if (Array.isArray(viewType.value)) {
107
+ viewType.value = [val]
108
+ } else {
109
+ viewType.value = val
110
+ }
111
+ nextTick(() => { isProgrammaticSet.value = false })
112
+ }
113
+
114
+ watch(viewType, () => {
115
+ if (!isProgrammaticSet.value) userOverrodeView.value = true
116
+ })
117
+
118
+ function parsePrefer(val: unknown): boolean | number {
119
+ if (val === true) return true
120
+ const n = Number(val)
121
+ return Number.isFinite(n) && n > 0 ? n : false
122
+ }
123
+
124
+ const computedPreferTable = computed<boolean | number>(() => {
125
+ const bp = display.name?.value
126
+ if (bp === 'xxl' && props.preferTableXxl !== undefined) return parsePrefer(props.preferTableXxl)
127
+ if (bp === 'xl' && props.preferTableXl !== undefined) return parsePrefer(props.preferTableXl)
128
+ if (bp === 'lg' && props.preferTableLg !== undefined) return parsePrefer(props.preferTableLg)
129
+ if (bp === 'md' && props.preferTableMd !== undefined) return parsePrefer(props.preferTableMd)
130
+ if (bp === 'sm' && props.preferTableSm !== undefined) return parsePrefer(props.preferTableSm)
131
+ return parsePrefer(props.preferTable)
132
+ })
133
+
134
+ const items = ref<Record<string, any>[]>([])
135
+ const search = ref<string>()
136
+ const currentItem = ref<Record<string, any> | undefined>(undefined)
137
+
138
+ function setSearch(keyword: string) {
139
+ search.value = keyword
140
+ }
141
+
142
+ const isDialogOpen = ref<boolean>(false)
143
+
144
+ watch(() => props.modelValue, (newValue) => {
145
+ if (!Array.isArray(newValue) || !newValue.every(item => typeof item === 'object')) {
146
+ items.value = []
147
+ }
148
+ else {
149
+ let maxKey = 0
150
+
151
+ newValue.forEach((item) => {
152
+ if (!item.hasOwnProperty(props.modelKey)) {
153
+ maxKey = Math.max(maxKey, ...newValue.map(i => i[props.modelKey] || 0))
154
+ item[props.modelKey] = maxKey + 1
155
+ }
156
+ })
157
+
158
+ items.value = newValue
159
+ }
160
+ }, { immediate: true })
161
+
162
+ watch(items, (newValue) => {
163
+ emit('update:modelValue', newValue)
164
+ }, { deep: true })
165
+
166
+ watch(
167
+ [() => items.value?.length, computedPreferTable, () => display.name?.value],
168
+ ([len, prefer]) => {
169
+ if (userOverrodeView.value) return // respect explicit user choice forever (until remount)
170
+
171
+ let target: 'iterator' | 'table' = 'iterator'
172
+ if (prefer === true) {
173
+ target = 'table'
174
+ } else if (typeof prefer === 'number') {
175
+ if (Number(len) >= prefer) target = 'table'
176
+ }
177
+
178
+ if (!viewType.value?.includes(target)) setViewTypeSafely(target)
179
+ },
180
+ { immediate: true }
181
+ )
182
+
183
+ const itemsPerPageInternal = ref<string | number>()
184
+ watch(() => props.itemsPerPage, (newValue) => {
185
+ if (newValue.toString().toLowerCase() == 'all') itemsPerPageInternal.value = '-1'
186
+ else if (newValue) itemsPerPageInternal.value = newValue
187
+ }, { immediate: true })
188
+
189
+ function createItem(item: Record<string, any>, callback?: FormDialogCallback) {
190
+ if (items.value.length > 0) item[props.modelKey] = Math.max(...items.value.map(i => i[props.modelKey] || 0)) + 1
191
+ else item[props.modelKey] = 1
192
+
193
+ items.value.push(item)
194
+
195
+ if (callback) callback.done()
196
+ }
197
+
198
+ function importItems(importItems: Record<string, any>[], callback?: FormDialogCallback) {
199
+ importItems.forEach((item) => {
200
+ createItem(item)
201
+ })
202
+ if (callback) callback.done()
203
+ }
204
+
205
+ function updateItem(newItem: Record<string, any>, callback?: FormDialogCallback) {
206
+ const index = items.value.findIndex(item => item[props.modelKey] === newItem[props.modelKey])
207
+
208
+ if (index !== -1) {
209
+ items.value[index] = newItem
210
+ }
211
+
212
+ if (callback) callback.done()
213
+ }
214
+
215
+ function moveUpItem(currentItem: Record<string, any>, callback?: FormDialogCallback) {
216
+ const index = items.value.findIndex(item => item[props.modelKey] === currentItem[props.modelKey])
217
+
218
+ if (index > 0) {
219
+ const temp = items.value[index - 1]
220
+ items.value[index - 1] = items.value[index]
221
+ items.value[index] = temp
222
+ }
223
+
224
+ if (callback) callback.done()
225
+ }
226
+
227
+ function moveDownItem(currentItem: Record<string, any>, callback?: FormDialogCallback) {
228
+ const index = items.value.findIndex(item => item[props.modelKey] === currentItem[props.modelKey])
229
+
230
+ if (index >= 0 && index < items.value.length - 1) {
231
+ const temp = items.value[index + 1]
232
+ items.value[index + 1] = items.value[index]
233
+ items.value[index] = temp
234
+ }
235
+
236
+ if (callback) callback.done()
237
+ }
238
+
239
+ function moveToItem(currentItem: Record<string, any>, callback?: FormDialogCallback) {
240
+ const index = items.value.findIndex(item => item[props.modelKey] === currentItem[props.modelKey]);
241
+
242
+ if (index !== -1) {
243
+ const newPosition = prompt("Enter the new position (0-based index):");
244
+ const parsedPosition = parseInt(<string>newPosition, 10);
245
+
246
+ if (isNaN(parsedPosition) || parsedPosition < 0 || parsedPosition >= items.value.length) {
247
+ alert("Invalid position entered. Please enter a number between 0 and " + (items.value.length - 1));
248
+ return
249
+ }
250
+
251
+ const [temp] = items.value.splice(index, 1);
252
+
253
+ items.value.splice(parsedPosition, 0, temp);
254
+ }
255
+
256
+ if (callback) callback.done();
257
+ }
258
+
259
+ function deleteItem(deleteItem: Record<string, any>, callback?: FormDialogCallback) {
260
+ const index = items.value.findIndex(item => item[props.modelKey] === deleteItem[props.modelKey])
261
+
262
+ if (index !== -1) {
263
+ items.value.splice(index, 1)
264
+ }
265
+
266
+ if (callback) callback.done()
267
+ }
268
+
269
+ function openDialog(item?: object) {
270
+ currentItem.value = item
271
+ nextTick(() => {
272
+ isDialogOpen.value = true
273
+ })
274
+ }
275
+
276
+ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpItem, moveDownItem,moveToItem,setSearch })
277
+
278
+ const isValid = computed(()=>{
279
+ return inputRef.value?.isValid
280
+ })
281
+
282
+ const errorMessages = computed(()=>{
283
+ return inputRef.value?.errorMessages
284
+ })
285
+
286
+ defineExpose({
287
+ errorMessages,
288
+ isValid,
289
+ reset: ()=>inputRef.value?.reset(),
290
+ resetValidation : ()=>inputRef.value?.resetValidation(),
291
+ validate : ()=>inputRef.value?.validate(),
292
+ operation
293
+ })
294
+ </script>
295
+
296
+ <template>
297
+ <v-input v-model="items" v-bind="plainAttrs" ref="inputRef">
298
+ <template #default="{isReadonly,isDisabled}">
299
+ <v-container fluid class="ma-0 pa-0">
300
+ <v-card>
301
+ <v-data-iterator
302
+ v-bind="plainAttrs"
303
+ v-model:items-per-page="itemsPerPageInternal"
304
+ :items="items"
305
+ :item-value="modelKey"
306
+ :search="search"
307
+ :loading="loading"
308
+ >
309
+ <template #default="defaultProps" v-if="viewType.includes('iterator')">
310
+ <slot
311
+ v-bind="defaultProps"
312
+ :operation="operation"
313
+ >
314
+ <v-container fluid>
315
+ <v-row>
316
+ <v-col
317
+ v-for="(item, index) in defaultProps.items"
318
+ :key="index"
319
+ :cols="cols"
320
+ :sm="sm"
321
+ :md="md"
322
+ :lg="lg"
323
+ :xl="xl"
324
+ :xxl="xxl"
325
+ >
326
+ <slot
327
+ name="item"
328
+ :item="item"
329
+ :operation="operation"
330
+ />
331
+ </v-col>
332
+ </v-row>
333
+ </v-container>
334
+ </slot>
335
+ </template>
336
+ <template #loader="loaderProps">
337
+ <slot
338
+ name="loader"
339
+ v-bind="loaderProps"
340
+ >
341
+ <v-container fluid>
342
+ <v-row>
343
+ <v-col
344
+ v-for="key in itemsPerPage"
345
+ :key="key"
346
+ :cols="cols"
347
+ :sm="sm"
348
+ :md="md"
349
+ :lg="lg"
350
+ :xl="xl"
351
+ :xxl="xxl"
352
+ >
353
+ <slot name="loaderItem">
354
+ <v-skeleton-loader
355
+ type="paragraph"
356
+ />
357
+ </slot>
358
+ </v-col>
359
+ </v-row>
360
+ </v-container>
361
+ </slot>
362
+ </template>
363
+ <template #header="headerProps">
364
+ <slot
365
+ name="header"
366
+ v-bind="headerProps"
367
+ :items="items"
368
+ :operation="operation"
369
+ >
370
+ <VToolbar :color="toolbarColor">
371
+ <v-row
372
+ justify="end"
373
+ class="ma-1"
374
+ dense
375
+ no-gutters
376
+ align="center"
377
+ >
378
+ <v-col cols="7">
379
+ <VToolbarTitle class="pl-3">
380
+ <slot name="title">
381
+ {{ title }}
382
+ </slot>
383
+ </VToolbarTitle>
384
+ </v-col>
385
+ <v-col cols="5">
386
+ <slot name="search" :items="items" :operation="operation" v-if="props.searchable">
387
+ <VTextField
388
+ v-model="search"
389
+ class="justify-end w-100"
390
+ density="compact"
391
+ hide-details
392
+ placeholder="ค้นหา"
393
+ clearable
394
+ variant="solo"
395
+ />
396
+ </slot>
397
+ </v-col>
398
+ </v-row>
399
+
400
+ <VToolbarItems>
401
+ <slot name="toolbarItems" :items="items" :operation="operation"/>
402
+ <ImportCSV
403
+ v-if="props.importable && !(isReadonly.value || isDisabled.value)"
404
+ icon="mdi:mdi-file-upload"
405
+ variant="flat"
406
+ @import="importItems"
407
+ :color="toolbarColor"
408
+ />
409
+ <ExportCSV
410
+ v-if="props.exportable && items.length && !(isReadonly.value || isDisabled.value)"
411
+ icon="mdi:mdi-file-download"
412
+ variant="flat"
413
+ :file-name="title"
414
+ :model-value="items"
415
+ :color="toolbarColor"
416
+ />
417
+ <VBtn
418
+ v-if="props.insertable && !(isReadonly.value || isDisabled.value)"
419
+ :color="toolbarColor"
420
+ prepend-icon="mdi:mdi-plus"
421
+ variant="flat"
422
+ @click="openDialog()"
423
+ >
424
+ add
425
+ </VBtn>
426
+ <v-row align="center" class="mr-2 ml-2" v-if="viewSwitch">
427
+ <v-btn-toggle v-model="viewType" :multiple="viewSwitchMultiple" mandatory>
428
+ <v-btn icon="mdi mdi-view-grid-outline" value="iterator"></v-btn>
429
+ <v-btn icon="mdi mdi-table-large" value="table"></v-btn>
430
+ </v-btn-toggle>
431
+ </v-row>
432
+ </VToolbarItems>
433
+ </VToolbar>
434
+ </slot>
435
+ <v-data-table
436
+ v-bind="plainAttrs"
437
+ color="primary"
438
+ :items="items"
439
+ :search="search"
440
+ :loading="loading"
441
+ v-if="viewType.includes('table')"
442
+ >
443
+ <!-- @ts-ignore -->
444
+ <template
445
+ v-for="(_, name, index) in (tableSlots as {})"
446
+ :key="index"
447
+ #[name]="slotData"
448
+ >
449
+ <slot
450
+ :name="name"
451
+ v-bind="((slotData || {}) as object)"
452
+ :operation="operation"
453
+ :isReadonly="isReadonly"
454
+ :isDisabled="isDisabled"
455
+ />
456
+ </template>
457
+ <template
458
+ v-if="!$slots['item.operation'] && !(isReadonly.value || isDisabled.value)"
459
+ #item.operation="props"
460
+ >
461
+ <v-icon density="compact" :disabled="props.index==0" @click="moveUpItem(props.item)">mdi:mdi-arrow-up-thick</v-icon>
462
+ <v-icon density="compact" @click="moveToItem(props.item)">fa:fas fa-arrow-right-to-bracket</v-icon>
463
+ <v-icon density="compact" :disabled="props.index==items.length-1" @click="moveDownItem(props.item)">mdi:mdi-arrow-down-thick</v-icon>
464
+ </template>
465
+ <template
466
+ v-if="!$slots['item.action'] && !(isReadonly.value || isDisabled.value)"
467
+ #item.action="{ item }"
468
+ >
469
+ <v-btn
470
+ variant="flat"
471
+ density="compact"
472
+ icon="mdi:mdi-note-edit"
473
+ @click="openDialog(item)"
474
+ />
475
+ <v-btn
476
+ variant="flat"
477
+ density="compact"
478
+ icon="mdi:mdi-delete"
479
+ @click="deleteItem(item)"
480
+ />
481
+ </template>
482
+ </v-data-table>
483
+ </template>
484
+ <template #footer="footerProps" v-if="viewType.includes('iterator')">
485
+ <v-container fluid>
486
+ <v-row
487
+ align-content="center"
488
+ justify="end"
489
+ dense
490
+ >
491
+ <v-spacer />
492
+ <v-select
493
+ v-model="itemsPerPageInternal"
494
+ density="compact"
495
+ variant="outlined"
496
+ :items="[6, 12, 18, 24, 30, { title: 'All', value: '-1' }]"
497
+ min-width="220"
498
+ max-width="220"
499
+ hide-details
500
+ >
501
+ <template #prepend>
502
+ Items per page:
503
+ </template>
504
+ </v-select>
505
+ <v-pagination
506
+ density="compact"
507
+ :length="footerProps.pageCount"
508
+ total-visible="6"
509
+ show-first-last-page
510
+ @first="footerProps.setPage(1)"
511
+ @last="footerProps.setPage(footerProps.pageCount)"
512
+ @update:model-value="footerProps.setPage"
513
+ />
514
+ </v-row>
515
+ </v-container>
516
+ </template >
517
+ </v-data-iterator>
518
+ <FormDialog
519
+ v-model="isDialogOpen"
520
+ :title="title"
521
+ :fullscreen="dialogFullscreen"
522
+ :initial-data="initialData"
523
+ :form-data="currentItem"
524
+ @create="createItem"
525
+ @update="updateItem"
526
+ >
527
+ <template #default="slotData">
528
+ <slot
529
+ name="form"
530
+ v-bind="slotData"
531
+ />
532
+ </template>
533
+ </FormDialog>
534
+ </v-card>
535
+ </v-container>
536
+ </template>
537
+ </v-input>
538
+ </template>