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