@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.
- package/README.md +115 -115
- package/dist/module.json +1 -1
- package/dist/runtime/components/Alert.vue +58 -58
- package/dist/runtime/components/BarcodeReader.vue +130 -130
- package/dist/runtime/components/ExportCSV.vue +110 -110
- package/dist/runtime/components/FileBtn.vue +79 -79
- package/dist/runtime/components/ImportCSV.vue +151 -151
- package/dist/runtime/components/MrzReader.vue +168 -168
- package/dist/runtime/components/SplitterPanel.vue +67 -67
- package/dist/runtime/components/TabsGroup.vue +39 -39
- package/dist/runtime/components/TextBarcode.vue +66 -66
- package/dist/runtime/components/device/IdCardButton.vue +95 -95
- package/dist/runtime/components/device/IdCardWebSocket.vue +207 -207
- package/dist/runtime/components/device/Scanner.vue +350 -350
- package/dist/runtime/components/dialog/Confirm.vue +112 -112
- package/dist/runtime/components/dialog/Host.vue +88 -88
- package/dist/runtime/components/dialog/Index.vue +84 -84
- package/dist/runtime/components/dialog/Loading.vue +51 -51
- package/dist/runtime/components/dialog/default/Confirm.vue +112 -112
- package/dist/runtime/components/dialog/default/Loading.vue +60 -60
- package/dist/runtime/components/dialog/default/Notify.vue +82 -82
- package/dist/runtime/components/dialog/default/Printing.vue +46 -46
- package/dist/runtime/components/dialog/default/VerifyUser.vue +144 -144
- package/dist/runtime/components/document/Form.vue +50 -50
- package/dist/runtime/components/document/TemplateBuilder.vue +536 -536
- package/dist/runtime/components/form/ActionPad.vue +156 -156
- package/dist/runtime/components/form/Birthdate.vue +116 -116
- package/dist/runtime/components/form/CheckboxGroup.vue +99 -99
- package/dist/runtime/components/form/CodeEditor.vue +45 -45
- package/dist/runtime/components/form/Date.vue +270 -270
- package/dist/runtime/components/form/DateTime.vue +220 -220
- package/dist/runtime/components/form/Dialog.vue +178 -178
- package/dist/runtime/components/form/EditPad.vue +157 -157
- package/dist/runtime/components/form/File.vue +295 -295
- package/dist/runtime/components/form/Hidden.vue +44 -44
- package/dist/runtime/components/form/Iterator.vue +538 -538
- package/dist/runtime/components/form/Login.vue +143 -143
- package/dist/runtime/components/form/Pad.vue +399 -399
- package/dist/runtime/components/form/SignPad.vue +226 -226
- package/dist/runtime/components/form/System.vue +34 -34
- package/dist/runtime/components/form/Table.vue +391 -391
- package/dist/runtime/components/form/TableData.vue +236 -236
- package/dist/runtime/components/form/Time.vue +177 -177
- package/dist/runtime/components/form/images/Capture.vue +245 -245
- package/dist/runtime/components/form/images/Edit.vue +133 -133
- package/dist/runtime/components/form/images/Field.vue +331 -331
- package/dist/runtime/components/form/images/Pad.vue +54 -54
- package/dist/runtime/components/label/Date.vue +37 -37
- package/dist/runtime/components/label/DateAgo.vue +102 -102
- package/dist/runtime/components/label/DateCount.vue +152 -152
- package/dist/runtime/components/label/Field.vue +111 -111
- package/dist/runtime/components/label/FormatMoney.vue +37 -37
- package/dist/runtime/components/label/Mask.vue +46 -46
- package/dist/runtime/components/label/Object.vue +21 -21
- package/dist/runtime/components/master/Autocomplete.vue +89 -89
- package/dist/runtime/components/master/Combobox.vue +88 -88
- package/dist/runtime/components/master/RadioGroup.vue +90 -90
- package/dist/runtime/components/master/Select.vue +70 -70
- package/dist/runtime/components/master/label.vue +55 -55
- package/dist/runtime/components/model/Autocomplete.vue +91 -91
- package/dist/runtime/components/model/Combobox.vue +90 -90
- package/dist/runtime/components/model/Pad.vue +114 -114
- package/dist/runtime/components/model/Select.vue +78 -84
- package/dist/runtime/components/model/Table.vue +370 -370
- package/dist/runtime/components/model/iterator.vue +497 -497
- package/dist/runtime/components/model/label.vue +58 -58
- package/dist/runtime/components/pdf/Print.vue +75 -75
- package/dist/runtime/components/pdf/View.vue +146 -146
- package/dist/runtime/composables/dialog.d.ts +1 -1
- package/dist/runtime/composables/graphql.d.ts +1 -1
- package/dist/runtime/composables/graphqlModel.d.ts +9 -9
- package/dist/runtime/composables/graphqlModelItem.d.ts +7 -7
- package/dist/runtime/composables/graphqlModelOperation.d.ts +6 -6
- package/dist/runtime/composables/userPermission.d.ts +1 -1
- package/dist/runtime/labs/Calendar.vue +99 -99
- package/dist/runtime/labs/form/EditMobile.vue +152 -152
- package/dist/runtime/labs/form/TextFieldMask.vue +43 -43
- package/dist/runtime/plugins/clientConfig.d.ts +1 -1
- package/dist/runtime/plugins/default.d.ts +1 -1
- package/dist/runtime/plugins/dialogManager.d.ts +1 -1
- package/dist/runtime/plugins/permission.d.ts +1 -1
- package/dist/runtime/types/alert.d.ts +11 -11
- package/dist/runtime/types/clientConfig.d.ts +13 -13
- package/dist/runtime/types/dialogManager.d.ts +35 -35
- package/dist/runtime/types/formDialog.d.ts +5 -5
- package/dist/runtime/types/graphqlOperation.d.ts +23 -23
- package/dist/runtime/types/menu.d.ts +31 -31
- package/dist/runtime/types/modules.d.ts +7 -7
- package/dist/runtime/types/permission.d.ts +13 -13
- package/package.json +131 -131
- package/scripts/enrich-vue-docs-from-ai.mjs +197 -197
- package/scripts/generate-ai-summary.mjs +321 -321
- package/scripts/generate-composables-md.mjs +129 -129
- package/scripts/postInstall.cjs +70 -70
- package/templates/.codegen/codegen.ts +32 -32
- 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>
|