@wishbone-media/spark 0.14.0 → 0.15.0
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/dist/index.css +1 -0
- package/dist/index.js +1735 -2669
- package/formkit.theme.mjs +53 -53
- package/package.json +8 -1
- package/src/assets/css/spark-table.css +69 -0
- package/src/components/SparkButton.vue +1 -1
- package/src/components/SparkTable.vue +368 -0
- package/src/components/SparkTablePaginationDetails.vue +26 -0
- package/src/components/SparkTablePaginationPaging.vue +102 -0
- package/src/components/SparkTablePaginationPerPage.vue +47 -0
- package/src/components/SparkTableToolbar.vue +33 -0
- package/src/components/index.js +7 -0
- package/src/components/plugins/SparkTableDatePicker.vue +122 -0
- package/src/components/plugins/SparkTableFilterButtons.vue +123 -0
- package/src/components/plugins/SparkTableFilterSelect.vue +116 -0
- package/src/components/plugins/SparkTableReset.vue +123 -0
- package/src/components/plugins/SparkTableSearch.vue +107 -0
- package/src/components/plugins/index.js +5 -0
- package/src/containers/SparkDefaultContainer.vue +6 -1
- package/src/index.js +1 -0
- package/src/plugins/fontawesome.js +6 -0
- package/src/utils/index.js +4 -0
- package/src/utils/sparkTable/header-title.js +10 -0
- package/src/utils/sparkTable/header.js +137 -0
- package/src/utils/sparkTable/plugin-manager.js +67 -0
- package/src/utils/sparkTable/renderers/actions.js +125 -0
- package/src/utils/sparkTable/renderers/badge.js +66 -0
- package/src/utils/sparkTable/renderers/date.js +102 -0
- package/src/utils/sparkTable/renderers/image.js +61 -0
- package/src/utils/sparkTable/renderers/index.js +82 -0
- package/src/utils/sparkTable/renderers/link.js +69 -0
- package/src/utils/sparkTable/update-row.js +5 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="spark-table">
|
|
3
|
+
<!-- Header Toolbar: All plugins flow left to right -->
|
|
4
|
+
<spark-table-toolbar v-if="sparkTable.computed.ready" position="header">
|
|
5
|
+
<component
|
|
6
|
+
v-for="plugin in headerPlugins"
|
|
7
|
+
:key="plugin.name"
|
|
8
|
+
:is="pluginComponentsByType[plugin.config.type]"
|
|
9
|
+
:class="plugin.config.align ? `self-${plugin.config.align}` : ''"
|
|
10
|
+
:spark-table="sparkTable"
|
|
11
|
+
:config="plugin.config"
|
|
12
|
+
/>
|
|
13
|
+
<slot name="header" :spark-table="sparkTable" :loading="loading" :error="error"></slot>
|
|
14
|
+
</spark-table-toolbar>
|
|
15
|
+
|
|
16
|
+
<!-- Table Grid -->
|
|
17
|
+
<HotTable theme-name="ht-theme-classic" ref="table" :settings="sparkTable.tableSettings" />
|
|
18
|
+
|
|
19
|
+
<!-- Footer Toolbar: Pagination always on the right -->
|
|
20
|
+
<spark-table-toolbar v-if="sparkTable.computed.ready" position="footer">
|
|
21
|
+
<spark-table-pagination-details
|
|
22
|
+
:spark-table="sparkTable"
|
|
23
|
+
/>
|
|
24
|
+
<div class="flex items-center gap-x-3">
|
|
25
|
+
<spark-table-pagination-paging
|
|
26
|
+
:spark-table="sparkTable"
|
|
27
|
+
@paginate="sparkTable.methods.applyParams"
|
|
28
|
+
/>
|
|
29
|
+
<spark-table-pagination-per-page
|
|
30
|
+
:spark-table="sparkTable"
|
|
31
|
+
@paginate="sparkTable.methods.applyParams"
|
|
32
|
+
/>
|
|
33
|
+
<slot name="footer" :spark-table="sparkTable" :loading="loading" :error="error"></slot>
|
|
34
|
+
</div>
|
|
35
|
+
</spark-table-toolbar>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script>
|
|
40
|
+
const defaultOptions = {
|
|
41
|
+
perPages: [100, 200, 500],
|
|
42
|
+
limit: 100,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const defaultParams = {
|
|
46
|
+
page: 1,
|
|
47
|
+
limit: defaultOptions.limit,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const defaultTableSettings = {
|
|
51
|
+
colHeaders: true,
|
|
52
|
+
filters: false,
|
|
53
|
+
stretchH: 'all',
|
|
54
|
+
colWidths: 50,
|
|
55
|
+
renderAllRows: true,
|
|
56
|
+
readOnly: true,
|
|
57
|
+
tableClassName: 'spark-table-table',
|
|
58
|
+
readOnlyCellClassName: 'read-only',
|
|
59
|
+
licenseKey: 'non-commercial-and-evaluation',
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
<script setup>
|
|
63
|
+
import { computed, onMounted, ref, reactive, inject } from 'vue'
|
|
64
|
+
import nprogress from 'nprogress'
|
|
65
|
+
import { has, get, find } from 'lodash'
|
|
66
|
+
import { HotTable } from '@handsontable/vue3'
|
|
67
|
+
import 'handsontable/styles/handsontable.css'
|
|
68
|
+
import 'handsontable/styles/ht-theme-classic.css'
|
|
69
|
+
import {
|
|
70
|
+
registerPlugin,
|
|
71
|
+
AutoColumnSize,
|
|
72
|
+
CopyPaste,
|
|
73
|
+
StretchColumns,
|
|
74
|
+
NestedHeaders,
|
|
75
|
+
} from 'handsontable/plugins'
|
|
76
|
+
import { registerAllCellTypes } from 'handsontable/cellTypes'
|
|
77
|
+
import { watchDebounced } from '@vueuse/core'
|
|
78
|
+
import { customiseHeader } from '@/utils/sparkTable/header.js'
|
|
79
|
+
import { registerSparkRenderers } from '@/utils/sparkTable/renderers'
|
|
80
|
+
import { updateRow } from '@/utils/sparkTable/update-row.js'
|
|
81
|
+
import SparkTablePaginationDetails from './SparkTablePaginationDetails.vue'
|
|
82
|
+
import SparkTablePaginationPaging from './SparkTablePaginationPaging.vue'
|
|
83
|
+
import SparkTablePaginationPerPage from './SparkTablePaginationPerPage.vue'
|
|
84
|
+
import SparkTableToolbar from './SparkTableToolbar.vue'
|
|
85
|
+
import { SparkTableSearch, SparkTableFilterSelect, SparkTableFilterButtons, SparkTableDatePicker, SparkTableReset } from './plugins'
|
|
86
|
+
|
|
87
|
+
const props = defineProps({
|
|
88
|
+
url: {
|
|
89
|
+
type: String,
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
dataProvider: {
|
|
93
|
+
type: Function,
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
dataTransformer: {
|
|
97
|
+
type: Function,
|
|
98
|
+
|
|
99
|
+
default: (data, props) => {
|
|
100
|
+
const hasNestedHeaders = get(props.settings, 'nestedHeaders')
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
data: hasNestedHeaders
|
|
104
|
+
? data.data.data.map((row) => {
|
|
105
|
+
const columnKeys = props.settings.columnKeys || []
|
|
106
|
+
return columnKeys.map((key) => row[key])
|
|
107
|
+
})
|
|
108
|
+
: data.data.data,
|
|
109
|
+
meta: {
|
|
110
|
+
last_page: data.data.last_page,
|
|
111
|
+
from: data.data.from,
|
|
112
|
+
to: data.data.to,
|
|
113
|
+
total: data.data.total,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
params: {
|
|
120
|
+
type: Object,
|
|
121
|
+
default() {
|
|
122
|
+
return defaultParams
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
options: {
|
|
127
|
+
type: Object,
|
|
128
|
+
default() {
|
|
129
|
+
return defaultOptions
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
settings: {
|
|
134
|
+
type: Object,
|
|
135
|
+
default() {
|
|
136
|
+
return defaultTableSettings
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
plugins: {
|
|
141
|
+
type: Object,
|
|
142
|
+
default: () => ({}),
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
registerAllCellTypes()
|
|
147
|
+
registerPlugin(AutoColumnSize)
|
|
148
|
+
registerPlugin(CopyPaste)
|
|
149
|
+
registerPlugin(StretchColumns)
|
|
150
|
+
if (get(props, 'settings.nestedHeaders')) {
|
|
151
|
+
registerPlugin(NestedHeaders)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const emit = defineEmits([
|
|
155
|
+
'viewRow',
|
|
156
|
+
'editRow',
|
|
157
|
+
'deleteRow',
|
|
158
|
+
'ready',
|
|
159
|
+
'loading',
|
|
160
|
+
'load',
|
|
161
|
+
'error',
|
|
162
|
+
'page-change',
|
|
163
|
+
])
|
|
164
|
+
|
|
165
|
+
// Inject configured axios instance
|
|
166
|
+
const axios = inject('axios')
|
|
167
|
+
|
|
168
|
+
const table = ref(null)
|
|
169
|
+
const loading = ref(false)
|
|
170
|
+
const error = ref(null)
|
|
171
|
+
|
|
172
|
+
// Plugin component mapping by type
|
|
173
|
+
const pluginComponentsByType = {
|
|
174
|
+
search: SparkTableSearch,
|
|
175
|
+
filterSelect: SparkTableFilterSelect,
|
|
176
|
+
filterButtons: SparkTableFilterButtons,
|
|
177
|
+
datePicker: SparkTableDatePicker,
|
|
178
|
+
reset: SparkTableReset,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Header plugins: all enabled plugins flow left to right
|
|
182
|
+
const headerPlugins = computed(() => {
|
|
183
|
+
return Object.entries(props.plugins)
|
|
184
|
+
.filter(([_, config]) => config && config.enabled)
|
|
185
|
+
.map(([name, config]) => ({ name, config }))
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const sparkTable = reactive({
|
|
189
|
+
hotInstance: null,
|
|
190
|
+
url: props.url,
|
|
191
|
+
plugins: props.plugins,
|
|
192
|
+
response: {},
|
|
193
|
+
params: {
|
|
194
|
+
page: 1,
|
|
195
|
+
limit: props.options.limit,
|
|
196
|
+
},
|
|
197
|
+
methods: {
|
|
198
|
+
applyParams: (payload) => {
|
|
199
|
+
if (payload) {
|
|
200
|
+
sparkTable.params = {
|
|
201
|
+
...sparkTable.params,
|
|
202
|
+
...payload,
|
|
203
|
+
}
|
|
204
|
+
emit('page-change', sparkTable.params)
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
removeParam: async (prop) => {
|
|
209
|
+
if (sparkTable.params[prop]) {
|
|
210
|
+
delete sparkTable.params[prop]
|
|
211
|
+
await sparkTable.methods.loadTable()
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
loadTable: async (callback) => {
|
|
216
|
+
if (!table.value || !table.value.hotInstance) {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
loading.value = true
|
|
221
|
+
error.value = null
|
|
222
|
+
emit('loading', true)
|
|
223
|
+
|
|
224
|
+
nprogress.start()
|
|
225
|
+
|
|
226
|
+
sparkTable.hotInstance = table.value.hotInstance
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
if (props.dataProvider) {
|
|
230
|
+
const response = await props.dataProvider(sparkTable.computed.params)
|
|
231
|
+
sparkTable.response = props.dataTransformer(response, props)
|
|
232
|
+
} else if (props.url) {
|
|
233
|
+
const response = await axios.get(props.url, {
|
|
234
|
+
params: sparkTable.computed.params,
|
|
235
|
+
})
|
|
236
|
+
sparkTable.response = props.dataTransformer(response, props)
|
|
237
|
+
} else {
|
|
238
|
+
console.error('No data provider or URL provided')
|
|
239
|
+
loading.value = false
|
|
240
|
+
emit('loading', false)
|
|
241
|
+
nprogress.done()
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
sparkTable.hotInstance.updateData(sparkTable.response.data)
|
|
246
|
+
|
|
247
|
+
if (sparkTable.options.callback && typeof sparkTable.options.callback === 'function') {
|
|
248
|
+
await sparkTable.options.callback()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// @see: https://forum.handsontable.com/t/table-not-maintaning-width/3116/11
|
|
252
|
+
const autoColumnSize = sparkTable.hotInstance.getPlugin('autoColumnSize')
|
|
253
|
+
autoColumnSize.recalculateAllColumnsWidth()
|
|
254
|
+
|
|
255
|
+
emit('load', {
|
|
256
|
+
data: sparkTable.response.data,
|
|
257
|
+
meta: sparkTable.response.meta,
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
if (callback && typeof callback === 'function') {
|
|
261
|
+
callback()
|
|
262
|
+
}
|
|
263
|
+
} catch (e) {
|
|
264
|
+
error.value = e
|
|
265
|
+
emit('error', e)
|
|
266
|
+
console.error('Error loading table data:', e)
|
|
267
|
+
} finally {
|
|
268
|
+
loading.value = false
|
|
269
|
+
emit('loading', false)
|
|
270
|
+
nprogress.done()
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// can't use sparkTable.hotInstance here because the ref isn't ready
|
|
275
|
+
colToProp: (col) => table.value.hotInstance.colToProp(col),
|
|
276
|
+
|
|
277
|
+
fireEvent: (type, payload) => {
|
|
278
|
+
emit(type, payload)
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
getSettingsForProp: (prop) => find(get(sparkTable.tableSettings, `columns`), { data: prop }),
|
|
282
|
+
|
|
283
|
+
// Helper methods for easier param access
|
|
284
|
+
getParams: () => sparkTable.computed.params,
|
|
285
|
+
getParam: (key) => sparkTable.computed.params[key],
|
|
286
|
+
setParam: (key, value) => {
|
|
287
|
+
sparkTable.methods.applyParams({ [key]: value })
|
|
288
|
+
},
|
|
289
|
+
clearParam: (key) => sparkTable.methods.removeParam(key),
|
|
290
|
+
clearParams: (keys) => {
|
|
291
|
+
// Clear multiple params at once without triggering multiple reloads
|
|
292
|
+
const keysArray = Array.isArray(keys) ? keys : [keys]
|
|
293
|
+
keysArray.forEach(key => {
|
|
294
|
+
if (sparkTable.params[key] !== undefined) {
|
|
295
|
+
delete sparkTable.params[key]
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
// Trigger a single reload
|
|
299
|
+
sparkTable.methods.applyParams({ page: 1 })
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
computed: {
|
|
304
|
+
params: computed(() => ({
|
|
305
|
+
...props.params,
|
|
306
|
+
...sparkTable.params,
|
|
307
|
+
})),
|
|
308
|
+
|
|
309
|
+
ready: computed(() => has(sparkTable, 'response.meta.last_page')),
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
options: computed(() => ({
|
|
313
|
+
...defaultOptions,
|
|
314
|
+
...props.options,
|
|
315
|
+
})),
|
|
316
|
+
|
|
317
|
+
tableSettings: computed(() => ({
|
|
318
|
+
...defaultTableSettings,
|
|
319
|
+
...{
|
|
320
|
+
...{
|
|
321
|
+
nestedHeaders: get(props.settings, 'nestedHeaders', []),
|
|
322
|
+
},
|
|
323
|
+
...(!get(props.settings, 'nestedHeaders') && {
|
|
324
|
+
afterGetColHeader: (col, th) => customiseHeader(col, th, sparkTable),
|
|
325
|
+
}),
|
|
326
|
+
afterChange: (changes, source) => updateRow(changes, source, sparkTable),
|
|
327
|
+
},
|
|
328
|
+
...props.settings,
|
|
329
|
+
})),
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
watchDebounced(
|
|
333
|
+
() => props.params,
|
|
334
|
+
async () => {
|
|
335
|
+
sparkTable.params.page = 1
|
|
336
|
+
await sparkTable.methods.loadTable()
|
|
337
|
+
},
|
|
338
|
+
{ debounce: 50, maxWait: 1000 },
|
|
339
|
+
)
|
|
340
|
+
watchDebounced(
|
|
341
|
+
() => sparkTable.params,
|
|
342
|
+
async () => {
|
|
343
|
+
await sparkTable.methods.loadTable()
|
|
344
|
+
},
|
|
345
|
+
{ debounce: 50, maxWait: 1000 },
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
onMounted(async () => {
|
|
349
|
+
await sparkTable.methods.loadTable()
|
|
350
|
+
emit('ready')
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
registerSparkRenderers(sparkTable)
|
|
354
|
+
|
|
355
|
+
defineExpose({
|
|
356
|
+
refresh: () => sparkTable.methods.loadTable(),
|
|
357
|
+
getParams: () => sparkTable.methods.getParams(),
|
|
358
|
+
getParam: (key) => sparkTable.methods.getParam(key),
|
|
359
|
+
setParam: (key, value) => sparkTable.methods.setParam(key, value),
|
|
360
|
+
clearParam: (key) => sparkTable.methods.clearParam(key),
|
|
361
|
+
clearParams: (keys) => sparkTable.methods.clearParams(keys),
|
|
362
|
+
applyParams: (params) => sparkTable.methods.applyParams(params),
|
|
363
|
+
loading,
|
|
364
|
+
error,
|
|
365
|
+
sparkTable,
|
|
366
|
+
table,
|
|
367
|
+
})
|
|
368
|
+
</script>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="flex items-center gap-4 px-4 py-3">
|
|
4
|
+
<div class="shrink-0">
|
|
5
|
+
<div class="text-sm text-gray-700">
|
|
6
|
+
Showing
|
|
7
|
+
<span class="font-medium">{{ props.sparkTable.response.meta.from }}</span>
|
|
8
|
+
to
|
|
9
|
+
<span class="font-medium">{{ props.sparkTable.response.meta.to }}</span>
|
|
10
|
+
of
|
|
11
|
+
<span class="font-medium">{{ props.sparkTable.response.meta.total }}</span>
|
|
12
|
+
results
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup>
|
|
20
|
+
const props = defineProps({
|
|
21
|
+
sparkTable: {
|
|
22
|
+
type: Object,
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
</script>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="flex items-center gap-4 px-4 py-3">
|
|
4
|
+
<div class="shrink-0 ml-auto" v-if="pageRange.length > 1">
|
|
5
|
+
<div>
|
|
6
|
+
<nav
|
|
7
|
+
class="isolate inline-flex -space-x-px rounded-md shadow-xs bg-white"
|
|
8
|
+
aria-label="Pagination"
|
|
9
|
+
>
|
|
10
|
+
<a
|
|
11
|
+
:class="canPrev ? '' : 'disabled'"
|
|
12
|
+
href="#"
|
|
13
|
+
class="relative inline-flex items-center rounded-l-md px-2 py-[9px] text-gray-400 ring-1 ring-gray-300 ring-inset hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
|
|
14
|
+
@click.prevent="pageChanged(-1)"
|
|
15
|
+
>
|
|
16
|
+
<font-awesome-icon :icon="Icons.farChevronLeft" class="size-5" />
|
|
17
|
+
</a>
|
|
18
|
+
|
|
19
|
+
<a
|
|
20
|
+
v-for="page in pageRange"
|
|
21
|
+
:key="`page-${page}`"
|
|
22
|
+
href="#"
|
|
23
|
+
class="relative inline-flex items-center px-4 py-[9px] text-sm font-semibold ring-1 ring-inset"
|
|
24
|
+
:class="
|
|
25
|
+
sparkTable.params.page === page
|
|
26
|
+
? 'z-10 text-white bg-primary-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 hover:bg-primary-600'
|
|
27
|
+
: 'text-gray-900 hover:bg-gray-50 ring-gray-300 focus:z-20 focus:outline-offset-0'
|
|
28
|
+
"
|
|
29
|
+
@click="gotoPage(page)"
|
|
30
|
+
>
|
|
31
|
+
{{ page }}
|
|
32
|
+
</a>
|
|
33
|
+
|
|
34
|
+
<a
|
|
35
|
+
:class="canNext ? '' : 'disabled'"
|
|
36
|
+
@click.prevent="pageChanged(1)"
|
|
37
|
+
href="#"
|
|
38
|
+
class="relative inline-flex items-center rounded-r-md px-2 py-[9px] text-gray-400 ring-1 ring-gray-300 ring-inset hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
|
|
39
|
+
>
|
|
40
|
+
<font-awesome-icon :icon="Icons.farChevronRight" class="size-5" />
|
|
41
|
+
</a>
|
|
42
|
+
</nav>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script setup>
|
|
50
|
+
import { computed } from 'vue'
|
|
51
|
+
import { get, range } from 'lodash'
|
|
52
|
+
import { Icons } from '@/plugins/fontawesome.js'
|
|
53
|
+
|
|
54
|
+
const props = defineProps({
|
|
55
|
+
sparkTable: {
|
|
56
|
+
type: Object,
|
|
57
|
+
required: true,
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const emit = defineEmits(['paginate'])
|
|
62
|
+
|
|
63
|
+
const pageChanged = (change) => {
|
|
64
|
+
gotoPage(props.sparkTable.params.page + change)
|
|
65
|
+
}
|
|
66
|
+
const gotoPage = (targetPage) => {
|
|
67
|
+
if (targetPage < 1 || targetPage > props.sparkTable.response.meta.last_page) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
emit('paginate', {
|
|
72
|
+
page: targetPage,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Range
|
|
77
|
+
const lastPage = computed(() => {
|
|
78
|
+
if (!props.sparkTable.params.page) {
|
|
79
|
+
return 1
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return Math.ceil(props.sparkTable.params.page / 10) * 10
|
|
83
|
+
})
|
|
84
|
+
const pageRange = computed(() => {
|
|
85
|
+
if (!props.sparkTable.computed.ready) {
|
|
86
|
+
return []
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return range(
|
|
90
|
+
Math.floor((lastPage.value - 1) / 10) * 10 + 1,
|
|
91
|
+
lastPage.value > props.sparkTable.response.meta.last_page
|
|
92
|
+
? props.sparkTable.response.meta.last_page + 1
|
|
93
|
+
: lastPage.value + 1,
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Status
|
|
98
|
+
const canNext = computed(
|
|
99
|
+
() => props.sparkTable.params.page < get(props.sparkTable.response.meta, 'last_page', 1),
|
|
100
|
+
)
|
|
101
|
+
const canPrev = computed(() => props.sparkTable.params.page > 1)
|
|
102
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="spark-table-pagination-per-page">
|
|
3
|
+
<FormKit
|
|
4
|
+
:model-value="sparkTable.params.limit"
|
|
5
|
+
type="select"
|
|
6
|
+
:options="perPageOptions"
|
|
7
|
+
outer-class="!mb-0"
|
|
8
|
+
wrapper-class="!mb-0"
|
|
9
|
+
input-class="!py-2"
|
|
10
|
+
@input="limitChanged"
|
|
11
|
+
/>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup>
|
|
16
|
+
import { computed } from 'vue'
|
|
17
|
+
|
|
18
|
+
const props = defineProps({
|
|
19
|
+
sparkTable: {
|
|
20
|
+
type: Object,
|
|
21
|
+
required: true,
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits(['paginate'])
|
|
26
|
+
|
|
27
|
+
// Format options for FormKit select
|
|
28
|
+
const perPageOptions = computed(() => {
|
|
29
|
+
return props.sparkTable.options.perPages.map((option) => ({
|
|
30
|
+
label: String(option),
|
|
31
|
+
value: option,
|
|
32
|
+
}))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const limitChanged = (value) => {
|
|
36
|
+
emit('paginate', {
|
|
37
|
+
page: 1,
|
|
38
|
+
limit: parseInt(value),
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<style scoped>
|
|
44
|
+
.spark-table-pagination-per-page {
|
|
45
|
+
@apply flex items-center;
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="hasContent" :class="toolbarClass">
|
|
3
|
+
<slot></slot>
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup>
|
|
8
|
+
import { computed, useSlots } from 'vue'
|
|
9
|
+
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
position: {
|
|
12
|
+
type: String,
|
|
13
|
+
required: true,
|
|
14
|
+
validator: (value) => ['header', 'footer'].includes(value),
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const slots = useSlots()
|
|
19
|
+
|
|
20
|
+
const hasContent = computed(() => !!slots.default)
|
|
21
|
+
|
|
22
|
+
const toolbarClass = computed(() => {
|
|
23
|
+
const baseClasses = 'spark-table-toolbar flex flex-wrap items-center gap-x-5 px-5 w-full'
|
|
24
|
+
|
|
25
|
+
// Footer has pagination details on left, controls on right
|
|
26
|
+
if (props.position === 'footer') {
|
|
27
|
+
return `${baseClasses} spark-table-toolbar-footer justify-between`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Header flows left to right
|
|
31
|
+
return `${baseClasses} spark-table-toolbar-header gap-y-5 py-5`
|
|
32
|
+
})
|
|
33
|
+
</script>
|
package/src/components/index.js
CHANGED
|
@@ -7,3 +7,10 @@ export { default as SparkCard } from './SparkCard.vue'
|
|
|
7
7
|
export { default as SparkModalContainer } from './SparkModalContainer.vue'
|
|
8
8
|
export { default as SparkModalDialog } from './SparkModalDialog.vue'
|
|
9
9
|
export { default as SparkOverlay } from './SparkOverlay.vue'
|
|
10
|
+
export { default as SparkTable } from './SparkTable.vue'
|
|
11
|
+
export { default as SparkTablePaginationPaging } from './SparkTablePaginationPaging.vue'
|
|
12
|
+
export { default as SparkTablePaginationPerPage } from './SparkTablePaginationPerPage.vue'
|
|
13
|
+
export { default as SparkTableToolbar } from './SparkTableToolbar.vue'
|
|
14
|
+
|
|
15
|
+
// Table plugins
|
|
16
|
+
export * from './plugins'
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="spark-table-date-picker">
|
|
3
|
+
<FormKit
|
|
4
|
+
v-model="selectedDate"
|
|
5
|
+
type="datepicker"
|
|
6
|
+
:label="label"
|
|
7
|
+
:placeholder="config.placeholder || 'Select date'"
|
|
8
|
+
:overlay="true"
|
|
9
|
+
value-format="YYYY-MM-DD"
|
|
10
|
+
format="YYYY-MM-DD"
|
|
11
|
+
outer-class="!mb-0"
|
|
12
|
+
wrapper-class="!mb-0"
|
|
13
|
+
v-bind="config.formkitProps || {}"
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup>
|
|
19
|
+
/**
|
|
20
|
+
* SparkTableDatePicker Plugin Component
|
|
21
|
+
*
|
|
22
|
+
* Provides a single date picker that applies date filter parameters to the SparkTable.
|
|
23
|
+
* For date ranges, use two separate datePicker plugin instances (startDate and endDate).
|
|
24
|
+
*
|
|
25
|
+
* @component
|
|
26
|
+
* @example
|
|
27
|
+
* // Single date filter
|
|
28
|
+
* const plugins = {
|
|
29
|
+
* createdDate: {
|
|
30
|
+
* type: 'datePicker',
|
|
31
|
+
* enabled: true,
|
|
32
|
+
* key: 'created_at',
|
|
33
|
+
* label: 'Created Date',
|
|
34
|
+
* placeholder: 'Select date',
|
|
35
|
+
* position: 'header-right',
|
|
36
|
+
* },
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Date range using two pickers
|
|
41
|
+
* const plugins = {
|
|
42
|
+
* startDate: {
|
|
43
|
+
* type: 'datePicker',
|
|
44
|
+
* enabled: true,
|
|
45
|
+
* key: 'created_at_start',
|
|
46
|
+
* label: 'Start Date',
|
|
47
|
+
* position: 'header-right',
|
|
48
|
+
* },
|
|
49
|
+
* endDate: {
|
|
50
|
+
* type: 'datePicker',
|
|
51
|
+
* enabled: true,
|
|
52
|
+
* key: 'created_at_end',
|
|
53
|
+
* label: 'End Date',
|
|
54
|
+
* position: 'header-right',
|
|
55
|
+
* },
|
|
56
|
+
* }
|
|
57
|
+
*/
|
|
58
|
+
import { ref, watch } from 'vue'
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Object} SparkTableDatePickerConfig
|
|
62
|
+
* @property {string} type - Plugin type: 'datePicker'
|
|
63
|
+
* @property {boolean} enabled - Enable the plugin
|
|
64
|
+
* @property {string} key - Date filter key (used in query params)
|
|
65
|
+
* @property {string} [label] - Label displayed above the date picker
|
|
66
|
+
* @property {string} [placeholder='Select date'] - Placeholder text
|
|
67
|
+
* @property {string} [param] - Custom query param (defaults to filter[key])
|
|
68
|
+
* @property {string} [position='header-left'] - Toolbar position
|
|
69
|
+
* @property {Object} [formkitProps] - Additional FormKit props to pass through
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
const props = defineProps({
|
|
73
|
+
/** SparkTable instance object */
|
|
74
|
+
sparkTable: {
|
|
75
|
+
type: Object,
|
|
76
|
+
required: true,
|
|
77
|
+
},
|
|
78
|
+
/** Date picker plugin configuration */
|
|
79
|
+
config: {
|
|
80
|
+
type: Object,
|
|
81
|
+
required: true,
|
|
82
|
+
validator: (config) => {
|
|
83
|
+
return config.key
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const selectedDate = ref(null)
|
|
89
|
+
const label = props.config.label || null
|
|
90
|
+
const param = props.config.param || `filter[${props.config.key}]`
|
|
91
|
+
|
|
92
|
+
// Initialize from existing params
|
|
93
|
+
selectedDate.value = props.sparkTable.params[param] || null
|
|
94
|
+
|
|
95
|
+
// Watch for date changes
|
|
96
|
+
watch(selectedDate, (newValue) => {
|
|
97
|
+
if (newValue) {
|
|
98
|
+
props.sparkTable.methods.applyParams({
|
|
99
|
+
page: 1,
|
|
100
|
+
[param]: newValue,
|
|
101
|
+
})
|
|
102
|
+
} else {
|
|
103
|
+
props.sparkTable.methods.removeParam(param)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Watch for external param changes
|
|
108
|
+
watch(
|
|
109
|
+
() => props.sparkTable.params[param],
|
|
110
|
+
(newValue) => {
|
|
111
|
+
if (!newValue && selectedDate.value) {
|
|
112
|
+
selectedDate.value = null
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<style scoped>
|
|
119
|
+
.spark-table-date-picker {
|
|
120
|
+
@apply flex items-center gap-2;
|
|
121
|
+
}
|
|
122
|
+
</style>
|