picocrank 1.22.1 → 1.23.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/package.json +5 -5
- package/router.js +19 -0
- package/vue/components/Table.vue +448 -104
- package/vue/components/TableColumnFilterPopover.vue +421 -0
- package/vue/composables/tableFilters.js +251 -0
- package/vue/composables/useLongPress.js +76 -0
- package/vue/views/App.vue +2 -0
- package/vue/views/DialogExample.vue +123 -0
- package/vue/views/TableExample.vue +34 -34
- package/vue/views/TableRemoteExample.vue +79 -0
- package/vue/views/Welcome.vue +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picocrank",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
|
|
5
5
|
"main": "vite.config.js",
|
|
6
6
|
"scripts": {
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
"@hugeicons/core-free-icons": "^4.2.3",
|
|
22
22
|
"@hugeicons/vue": "^1.0.7",
|
|
23
23
|
"@vitejs/plugin-vue": "^6.0.8",
|
|
24
|
-
"femtocrank": "^2.5.
|
|
24
|
+
"femtocrank": "^2.5.2",
|
|
25
25
|
"unplugin-vue-components": "^32.1.0",
|
|
26
|
-
"vite": "^8.2.
|
|
27
|
-
"vue": "^3.5.
|
|
26
|
+
"vite": "^8.2.1",
|
|
27
|
+
"vue": "^3.5.41",
|
|
28
28
|
"vue-router": "^5.2.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"semantic-release": "^25.0.
|
|
31
|
+
"semantic-release": "^25.0.9"
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://github.com/jamesread/picocrank#readme"
|
|
34
34
|
}
|
package/router.js
CHANGED
|
@@ -14,6 +14,7 @@ import { LayoutGridIcon } from '@hugeicons/core-free-icons';
|
|
|
14
14
|
import { StatusIcon } from '@hugeicons/core-free-icons';
|
|
15
15
|
import { PaintBoardIcon } from '@hugeicons/core-free-icons';
|
|
16
16
|
import { UserIcon } from '@hugeicons/core-free-icons';
|
|
17
|
+
import { InformationCircleIcon } from '@hugeicons/core-free-icons';
|
|
17
18
|
|
|
18
19
|
const routes = [
|
|
19
20
|
{
|
|
@@ -62,6 +63,15 @@ const routes = [
|
|
|
62
63
|
icon: TableIcon,
|
|
63
64
|
}
|
|
64
65
|
},
|
|
66
|
+
{
|
|
67
|
+
name: 'TableRemoteExample',
|
|
68
|
+
path: '/table-remote-example',
|
|
69
|
+
component: () => import('./vue/views/TableRemoteExample.vue'),
|
|
70
|
+
meta: {
|
|
71
|
+
title: 'Remote table',
|
|
72
|
+
icon: TableIcon,
|
|
73
|
+
}
|
|
74
|
+
},
|
|
65
75
|
{
|
|
66
76
|
name: 'ViewItem',
|
|
67
77
|
path: '/view-item/:id',
|
|
@@ -136,6 +146,15 @@ const routes = [
|
|
|
136
146
|
icon: StatusIcon,
|
|
137
147
|
}
|
|
138
148
|
},
|
|
149
|
+
{
|
|
150
|
+
name: 'DialogExample',
|
|
151
|
+
path: '/dialog-example',
|
|
152
|
+
component: () => import('./vue/views/DialogExample.vue'),
|
|
153
|
+
meta: {
|
|
154
|
+
title: 'Dialog',
|
|
155
|
+
icon: InformationCircleIcon,
|
|
156
|
+
}
|
|
157
|
+
},
|
|
139
158
|
{
|
|
140
159
|
name: 'TabsExample',
|
|
141
160
|
path: '/tabs-example',
|
package/vue/components/Table.vue
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<table class
|
|
2
|
+
<table class="row-hover" :class="{ loading: isLoading }">
|
|
3
3
|
<thead>
|
|
4
|
-
<th
|
|
5
|
-
|
|
4
|
+
<th
|
|
5
|
+
v-for="(header, index) in visibleHeaders"
|
|
6
|
+
:key="header.key || index"
|
|
7
|
+
:class="headerClasses(header)"
|
|
8
|
+
:style="{ width: header.width || 'auto' }"
|
|
9
|
+
@click="onHeaderClick(header, $event)"
|
|
10
|
+
@contextmenu.prevent="openFilter(header, $event)"
|
|
11
|
+
@pointerdown="onHeaderPointerDown(header, $event)"
|
|
12
|
+
@pointermove="longPress.onPointerMove"
|
|
13
|
+
@pointerup="longPress.onPointerUp"
|
|
14
|
+
@pointercancel="longPress.onPointerCancel"
|
|
15
|
+
@pointerleave="longPress.onPointerLeave"
|
|
16
|
+
>
|
|
17
|
+
<span class="header-label">{{ header.label || header.key }}</span>
|
|
6
18
|
|
|
7
|
-
<span v-if="header
|
|
19
|
+
<span v-if="filterable && isHeaderFiltered(header)" class="filter-indicator" title="Filtered" aria-hidden="true">⧩</span>
|
|
20
|
+
|
|
21
|
+
<span v-if="header.sortable" class="sort-indicator">
|
|
8
22
|
<span v-if="sortBy === header.key">
|
|
9
23
|
<span v-if="sortDir === 'asc'">▲</span>
|
|
10
24
|
<span v-else-if="sortDir === 'desc'">▼</span>
|
|
@@ -13,19 +27,21 @@
|
|
|
13
27
|
</th>
|
|
14
28
|
</thead>
|
|
15
29
|
<tbody>
|
|
16
|
-
<tr v-if="
|
|
17
|
-
<td :colspan="visibleHeaders.length">
|
|
30
|
+
<tr v-if="displayRows.length === 0">
|
|
31
|
+
<td :colspan="visibleHeaders.length">
|
|
32
|
+
<span v-if="isLoading">Loading…</span>
|
|
33
|
+
<span v-else>No items found</span>
|
|
34
|
+
</td>
|
|
18
35
|
</tr>
|
|
19
|
-
<tr v-else v-for="(row,
|
|
20
|
-
<td v-for="(header,
|
|
21
|
-
<component
|
|
22
|
-
|
|
23
|
-
:is="slotFor(header.key)"
|
|
36
|
+
<tr v-else v-for="(row, rowIndex) in displayRows" :key="rowIndex">
|
|
37
|
+
<td v-for="(header, cellIndex) in visibleHeaders" :key="header.key || cellIndex" :class="{ hidden: header.hidden }">
|
|
38
|
+
<component
|
|
39
|
+
v-if="slotFor(header.key)"
|
|
40
|
+
:is="slotFor(header.key)"
|
|
24
41
|
:class="{ hidden: header.hidden }"
|
|
25
|
-
:row="row"
|
|
42
|
+
:row="row"
|
|
26
43
|
:value="row[header.key]"
|
|
27
|
-
|
|
28
|
-
/>
|
|
44
|
+
/>
|
|
29
45
|
<span v-else>
|
|
30
46
|
{{ row[header.key] }}
|
|
31
47
|
</span>
|
|
@@ -33,100 +49,399 @@
|
|
|
33
49
|
</tr>
|
|
34
50
|
</tbody>
|
|
35
51
|
</table>
|
|
36
|
-
<div v-if="showPagination" class
|
|
37
|
-
<Pagination :total
|
|
52
|
+
<div v-if="showPagination" class="padding">
|
|
53
|
+
<Pagination :total="totalCount" v-model:page="page" v-model:page-size="pageSize" />
|
|
38
54
|
</div>
|
|
55
|
+
|
|
56
|
+
<TableColumnFilterPopover
|
|
57
|
+
v-if="filterable"
|
|
58
|
+
v-model:open="filterPopoverOpen"
|
|
59
|
+
:header="activeFilterHeader"
|
|
60
|
+
:anchor-el="activeFilterAnchor"
|
|
61
|
+
:value="activeFilterValue"
|
|
62
|
+
:rows="props.data"
|
|
63
|
+
:select-options="activeSelectOptions"
|
|
64
|
+
@apply="onFilterApply"
|
|
65
|
+
@clear="onFilterClear"
|
|
66
|
+
@cancel="onFilterCancel"
|
|
67
|
+
/>
|
|
39
68
|
</template>
|
|
40
69
|
|
|
41
70
|
<script setup>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
71
|
+
import { ref, computed, watch, onMounted, onBeforeUnmount, useSlots } from 'vue'
|
|
72
|
+
import Pagination from './Pagination.vue'
|
|
73
|
+
import TableColumnFilterPopover from './TableColumnFilterPopover.vue'
|
|
74
|
+
import { useLongPress } from '../composables/useLongPress.js'
|
|
75
|
+
import {
|
|
76
|
+
applyFilters,
|
|
77
|
+
buildSelectOptions,
|
|
78
|
+
cloneFilters,
|
|
79
|
+
isFilterActive,
|
|
80
|
+
sortRows,
|
|
81
|
+
} from '../composables/tableFilters.js'
|
|
82
|
+
|
|
83
|
+
const sortBy = ref(null)
|
|
84
|
+
const sortDir = ref('asc')
|
|
85
|
+
const page = ref(1)
|
|
86
|
+
const pageSize = ref(10)
|
|
87
|
+
|
|
88
|
+
const props = defineProps({
|
|
89
|
+
headers: {
|
|
90
|
+
type: Array,
|
|
91
|
+
default: () => ['id'],
|
|
92
|
+
},
|
|
93
|
+
data: {
|
|
94
|
+
type: Array,
|
|
95
|
+
default: () => [],
|
|
96
|
+
},
|
|
97
|
+
showPagination: {
|
|
98
|
+
type: Boolean,
|
|
99
|
+
default: true,
|
|
100
|
+
},
|
|
101
|
+
filterable: {
|
|
102
|
+
type: Boolean,
|
|
103
|
+
default: true,
|
|
104
|
+
},
|
|
105
|
+
filters: {
|
|
106
|
+
type: Object,
|
|
107
|
+
default: undefined,
|
|
108
|
+
},
|
|
109
|
+
fetchRows: {
|
|
110
|
+
type: Function,
|
|
111
|
+
default: null,
|
|
112
|
+
},
|
|
113
|
+
loading: {
|
|
114
|
+
type: Boolean,
|
|
115
|
+
default: false,
|
|
116
|
+
},
|
|
117
|
+
remoteDebounceMs: {
|
|
118
|
+
type: Number,
|
|
119
|
+
default: 300,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const emit = defineEmits([
|
|
124
|
+
'update:filters',
|
|
125
|
+
'filter-change',
|
|
126
|
+
'query-change',
|
|
127
|
+
'fetch-error',
|
|
128
|
+
])
|
|
129
|
+
|
|
130
|
+
const slots = useSlots()
|
|
131
|
+
const internalFilters = ref({})
|
|
132
|
+
const remoteRows = ref([])
|
|
133
|
+
const remoteTotal = ref(0)
|
|
134
|
+
const internalLoading = ref(false)
|
|
135
|
+
const fetchRequestId = ref(0)
|
|
136
|
+
const fetchAbortController = ref(null)
|
|
137
|
+
const fetchDebounceTimer = ref(null)
|
|
138
|
+
|
|
139
|
+
const filterPopoverOpen = ref(false)
|
|
140
|
+
const activeFilterHeader = ref(null)
|
|
141
|
+
const activeFilterAnchor = ref(null)
|
|
142
|
+
const activeFilterValue = ref(null)
|
|
143
|
+
|
|
144
|
+
const isRemote = computed(() => typeof props.fetchRows === 'function')
|
|
145
|
+
|
|
146
|
+
const activeFilters = computed(() => {
|
|
147
|
+
if (!props.filterable) {
|
|
148
|
+
return {}
|
|
149
|
+
}
|
|
150
|
+
return props.filters !== undefined ? props.filters : internalFilters.value
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const isLoading = computed(() => props.loading || internalLoading.value)
|
|
154
|
+
|
|
155
|
+
const visibleHeaders = computed(() => props.headers.filter((header) => !header.hidden))
|
|
156
|
+
|
|
157
|
+
const filteredItems = computed(() => {
|
|
158
|
+
if (isRemote.value) {
|
|
159
|
+
return remoteRows.value
|
|
160
|
+
}
|
|
161
|
+
return applyFilters(props.data, props.headers, activeFilters.value)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const sortedItems = computed(() => {
|
|
165
|
+
if (isRemote.value) {
|
|
166
|
+
return filteredItems.value
|
|
167
|
+
}
|
|
168
|
+
return sortRows(filteredItems.value, sortBy.value, sortDir.value)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const displayRows = computed(() => {
|
|
172
|
+
if (isRemote.value || !props.showPagination) {
|
|
173
|
+
return sortedItems.value
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const start = (page.value - 1) * pageSize.value
|
|
177
|
+
return sortedItems.value.slice(start, start + pageSize.value)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const totalCount = computed(() => {
|
|
181
|
+
if (isRemote.value) {
|
|
182
|
+
return remoteTotal.value
|
|
183
|
+
}
|
|
184
|
+
return sortedItems.value.length
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const activeSelectOptions = computed(() => {
|
|
188
|
+
if (!activeFilterHeader.value) {
|
|
189
|
+
return []
|
|
190
|
+
}
|
|
191
|
+
return buildSelectOptions(props.data, activeFilterHeader.value)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const queryParams = computed(() => ({
|
|
195
|
+
page: page.value,
|
|
196
|
+
pageSize: pageSize.value,
|
|
197
|
+
sortBy: sortBy.value,
|
|
198
|
+
sortDir: sortDir.value,
|
|
199
|
+
filters: cloneFilters(activeFilters.value),
|
|
200
|
+
}))
|
|
201
|
+
|
|
202
|
+
function slotFor(key) {
|
|
203
|
+
const named = slots[`cell-${key}`]
|
|
204
|
+
return named || slots.cell || null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isHeaderFilterable(header) {
|
|
208
|
+
return props.filterable && header.filterable !== false
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isHeaderFiltered(header) {
|
|
212
|
+
return isFilterActive(activeFilters.value?.[header.key])
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function headerClasses(header) {
|
|
216
|
+
return {
|
|
217
|
+
sortable: header.sortable,
|
|
218
|
+
filterable: isHeaderFilterable(header),
|
|
219
|
+
filtered: isHeaderFiltered(header),
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function setFilters(nextFilters) {
|
|
224
|
+
const cloned = cloneFilters(nextFilters)
|
|
225
|
+
if (props.filters === undefined) {
|
|
226
|
+
internalFilters.value = cloned
|
|
227
|
+
}
|
|
228
|
+
emit('update:filters', cloned)
|
|
229
|
+
emit('filter-change', cloned)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function emitQueryChange() {
|
|
233
|
+
emit('query-change', queryParams.value)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function toggleSort(header) {
|
|
237
|
+
if (!header.sortable) {
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (sortBy.value === header.key) {
|
|
242
|
+
sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'
|
|
243
|
+
} else {
|
|
244
|
+
sortBy.value = header.key
|
|
245
|
+
sortDir.value = 'asc'
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function onHeaderClick(header, event) {
|
|
250
|
+
if (longPress.suppressClick.value) {
|
|
251
|
+
longPress.onClick(event)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
toggleSort(header)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function openFilter(header, event) {
|
|
258
|
+
if (!isHeaderFilterable(header)) {
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
activeFilterHeader.value = header
|
|
263
|
+
activeFilterAnchor.value = event.currentTarget
|
|
264
|
+
activeFilterValue.value = activeFilters.value?.[header.key] ?? null
|
|
265
|
+
filterPopoverOpen.value = true
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const longPress = useLongPress((event) => {
|
|
269
|
+
const headerCell = event.currentTarget
|
|
270
|
+
const headerKey = headerCell?.dataset?.headerKey
|
|
271
|
+
const header = visibleHeaders.value.find((item) => item.key === headerKey)
|
|
272
|
+
if (header) {
|
|
273
|
+
openFilter(header, event)
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
function onHeaderPointerDown(header, event) {
|
|
278
|
+
if (!props.filterable) {
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
event.currentTarget.dataset.headerKey = header.key
|
|
282
|
+
longPress.onPointerDown(event)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function onFilterApply(filter) {
|
|
286
|
+
if (!activeFilterHeader.value) {
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const nextFilters = cloneFilters(activeFilters.value)
|
|
291
|
+
if (filter) {
|
|
292
|
+
nextFilters[activeFilterHeader.value.key] = filter
|
|
293
|
+
} else {
|
|
294
|
+
delete nextFilters[activeFilterHeader.value.key]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
setFilters(nextFilters)
|
|
298
|
+
page.value = 1
|
|
299
|
+
emitQueryChange()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function onFilterClear() {
|
|
303
|
+
if (!activeFilterHeader.value) {
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const nextFilters = cloneFilters(activeFilters.value)
|
|
308
|
+
delete nextFilters[activeFilterHeader.value.key]
|
|
309
|
+
setFilters(nextFilters)
|
|
310
|
+
page.value = 1
|
|
311
|
+
emitQueryChange()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function onFilterCancel() {
|
|
315
|
+
activeFilterHeader.value = null
|
|
316
|
+
activeFilterAnchor.value = null
|
|
317
|
+
activeFilterValue.value = null
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function runFetchRows() {
|
|
321
|
+
if (!isRemote.value) {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (fetchAbortController.value) {
|
|
326
|
+
fetchAbortController.value.abort()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const controller = new AbortController()
|
|
330
|
+
fetchAbortController.value = controller
|
|
331
|
+
const requestId = fetchRequestId.value + 1
|
|
332
|
+
fetchRequestId.value = requestId
|
|
333
|
+
internalLoading.value = true
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const result = await props.fetchRows(queryParams.value, { signal: controller.signal })
|
|
337
|
+
if (requestId !== fetchRequestId.value) {
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
remoteRows.value = Array.isArray(result?.rows) ? result.rows : []
|
|
342
|
+
remoteTotal.value = Number.isFinite(result?.total) ? result.total : remoteRows.value.length
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (error?.name === 'AbortError') {
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
emit('fetch-error', error)
|
|
348
|
+
if (requestId === fetchRequestId.value) {
|
|
349
|
+
remoteRows.value = []
|
|
350
|
+
remoteTotal.value = 0
|
|
351
|
+
}
|
|
352
|
+
} finally {
|
|
353
|
+
if (requestId === fetchRequestId.value) {
|
|
354
|
+
internalLoading.value = false
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function scheduleFetchRows() {
|
|
360
|
+
if (!isRemote.value) {
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (fetchDebounceTimer.value !== null) {
|
|
365
|
+
clearTimeout(fetchDebounceTimer.value)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fetchDebounceTimer.value = window.setTimeout(() => {
|
|
369
|
+
fetchDebounceTimer.value = null
|
|
370
|
+
runFetchRows()
|
|
371
|
+
}, props.remoteDebounceMs)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
watch(
|
|
375
|
+
() => [pageSize.value, sortBy.value, sortDir.value],
|
|
376
|
+
() => {
|
|
377
|
+
page.value = 1
|
|
378
|
+
if (isRemote.value) {
|
|
379
|
+
scheduleFetchRows()
|
|
380
|
+
}
|
|
381
|
+
emitQueryChange()
|
|
382
|
+
},
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
watch(page, () => {
|
|
386
|
+
if (isRemote.value) {
|
|
387
|
+
scheduleFetchRows()
|
|
388
|
+
}
|
|
389
|
+
emitQueryChange()
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
watch(
|
|
393
|
+
activeFilters,
|
|
394
|
+
() => {
|
|
395
|
+
page.value = 1
|
|
396
|
+
if (isRemote.value) {
|
|
397
|
+
scheduleFetchRows()
|
|
398
|
+
}
|
|
399
|
+
emitQueryChange()
|
|
400
|
+
},
|
|
401
|
+
{ deep: true },
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
watch(
|
|
405
|
+
() => props.filters,
|
|
406
|
+
(newFilters) => {
|
|
407
|
+
if (newFilters !== undefined) {
|
|
408
|
+
internalFilters.value = cloneFilters(newFilters)
|
|
128
409
|
}
|
|
410
|
+
},
|
|
411
|
+
{ deep: true },
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
watch(
|
|
415
|
+
() => props.filterable,
|
|
416
|
+
(enabled) => {
|
|
417
|
+
if (!enabled) {
|
|
418
|
+
filterPopoverOpen.value = false
|
|
419
|
+
onFilterCancel()
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
watch(sortedItems, () => {
|
|
425
|
+
if (!isRemote.value) {
|
|
426
|
+
page.value = 1
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
onMounted(() => {
|
|
431
|
+
emitQueryChange()
|
|
432
|
+
if (isRemote.value) {
|
|
433
|
+
runFetchRows()
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
onBeforeUnmount(() => {
|
|
438
|
+
if (fetchAbortController.value) {
|
|
439
|
+
fetchAbortController.value.abort()
|
|
440
|
+
}
|
|
441
|
+
if (fetchDebounceTimer.value !== null) {
|
|
442
|
+
clearTimeout(fetchDebounceTimer.value)
|
|
129
443
|
}
|
|
444
|
+
})
|
|
130
445
|
</script>
|
|
131
446
|
|
|
132
447
|
<style scoped>
|
|
@@ -135,8 +450,37 @@ table thead th.sortable:hover {
|
|
|
135
450
|
color: #0366d6;
|
|
136
451
|
}
|
|
137
452
|
|
|
138
|
-
|
|
139
|
-
|
|
453
|
+
table thead th.filterable {
|
|
454
|
+
cursor: context-menu;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
table thead th.filtered {
|
|
458
|
+
color: #0366d6;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
table.loading tbody {
|
|
462
|
+
opacity: 0.65;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.header-label {
|
|
466
|
+
margin-right: 0.25rem;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.filter-indicator {
|
|
470
|
+
display: inline-block;
|
|
471
|
+
margin-right: 0.25rem;
|
|
472
|
+
font-size: 0.85em;
|
|
473
|
+
opacity: 0.85;
|
|
140
474
|
}
|
|
141
475
|
|
|
476
|
+
.sort-indicator {
|
|
477
|
+
width: 1.5em;
|
|
478
|
+
display: inline-block;
|
|
479
|
+
text-align: center;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
td:first-child,
|
|
483
|
+
th:first-child {
|
|
484
|
+
padding-left: 1rem;
|
|
485
|
+
}
|
|
142
486
|
</style>
|