picocrank 1.31.1 → 1.33.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/docs/table.md +99 -0
- package/package.json +1 -1
- package/router.js +11 -0
- package/styles/picocrank-extensions.css +65 -0
- package/vue/components/Calendar.vue +41 -63
- package/vue/components/LoadingArea.spec.js +72 -0
- package/vue/components/LoadingArea.vue +176 -0
- package/vue/components/Table.spec.js +715 -0
- package/vue/components/Table.vue +974 -16
- package/vue/components/TableColumnFilterPopover.vue +56 -8
- package/vue/components/TableRowContextMenu.vue +267 -0
- package/vue/composables/tableFilters.js +6 -1
- package/vue/data/navSectionHubs.js +5 -0
- package/vue/examples/LoadingAreaExample.vue +175 -0
- package/vue/examples/TableExample.vue +156 -4
package/vue/components/Table.vue
CHANGED
|
@@ -7,6 +7,21 @@
|
|
|
7
7
|
<table class="row-hover" :class="{ loading: isLoading }">
|
|
8
8
|
<thead>
|
|
9
9
|
<tr>
|
|
10
|
+
<th
|
|
11
|
+
v-if="selectable"
|
|
12
|
+
class="table-select-col"
|
|
13
|
+
@click.stop
|
|
14
|
+
>
|
|
15
|
+
<label class="table-select-label">
|
|
16
|
+
<input
|
|
17
|
+
type="checkbox"
|
|
18
|
+
:checked="allDisplayRowsSelected"
|
|
19
|
+
:indeterminate="someDisplayRowsSelected"
|
|
20
|
+
:aria-label="allDisplayRowsSelected ? 'Deselect all rows on this page' : 'Select all rows on this page'"
|
|
21
|
+
@change="toggleSelectAllDisplayRows"
|
|
22
|
+
>
|
|
23
|
+
</label>
|
|
24
|
+
</th>
|
|
10
25
|
<th
|
|
11
26
|
v-for="(header, index) in visibleHeaders"
|
|
12
27
|
:key="header.key || index"
|
|
@@ -62,30 +77,68 @@
|
|
|
62
77
|
<tr
|
|
63
78
|
v-for="(row, rowIndex) in displayRows"
|
|
64
79
|
:key="resolveRowKey(row, rowIndex)"
|
|
65
|
-
:class="
|
|
80
|
+
:class="[
|
|
81
|
+
{
|
|
82
|
+
'row-clickable': rowClickable,
|
|
83
|
+
'row-selected': selectable && isRowSelected(row, rowIndex),
|
|
84
|
+
'row-context-menu-active': isRowContextMenuTarget(row, rowIndex),
|
|
85
|
+
},
|
|
86
|
+
resolveRowClass(row, rowIndex),
|
|
87
|
+
]"
|
|
88
|
+
:style="resolveRowStyle(row, rowIndex)"
|
|
66
89
|
@click="onRowClick(row, rowIndex, $event)"
|
|
90
|
+
@contextmenu="onRowContextMenu(row, rowIndex, $event)"
|
|
67
91
|
>
|
|
92
|
+
<td
|
|
93
|
+
v-if="selectable"
|
|
94
|
+
class="table-select-col"
|
|
95
|
+
data-row-click-ignore
|
|
96
|
+
@click.stop
|
|
97
|
+
>
|
|
98
|
+
<label class="table-select-label">
|
|
99
|
+
<input
|
|
100
|
+
type="checkbox"
|
|
101
|
+
:checked="isRowSelected(row, rowIndex)"
|
|
102
|
+
:aria-label="`Select row ${resolveRowKey(row, rowIndex)}`"
|
|
103
|
+
@click.stop="rememberSelectionCheckboxClick(row, rowIndex, $event)"
|
|
104
|
+
@change="onSelectionCheckboxChange(row, rowIndex)"
|
|
105
|
+
>
|
|
106
|
+
</label>
|
|
107
|
+
</td>
|
|
68
108
|
<td
|
|
69
109
|
v-for="(header, cellIndex) in visibleHeaders"
|
|
70
110
|
:key="header.key || cellIndex"
|
|
111
|
+
:ref="(element) => registerCellElement(element, row, header, rowIndex)"
|
|
112
|
+
v-bind="resolveCellAttrs(row, header, rowIndex)"
|
|
71
113
|
:colspan="bodyColumnColspan(cellIndex) || undefined"
|
|
72
114
|
:class="[
|
|
73
115
|
cellClasses(header),
|
|
74
116
|
header.class,
|
|
117
|
+
resolveCellClass(row, header, rowIndex),
|
|
118
|
+
{ 'table-active-cell': isActiveCell(row, header, rowIndex) },
|
|
75
119
|
isLastColumnWithOptions(cellIndex) ? 'table-last-column-with-options' : null,
|
|
76
120
|
]"
|
|
121
|
+
:style="resolveCellStyle(row, header, rowIndex)"
|
|
122
|
+
:tabindex="isActiveCell(row, header, rowIndex) ? 0 : -1"
|
|
123
|
+
@click="onCellClick(row, header, rowIndex, $event)"
|
|
77
124
|
>
|
|
78
125
|
<slot
|
|
79
126
|
v-if="slots[`cell-${header.key}`]"
|
|
80
127
|
:name="`cell-${header.key}`"
|
|
81
128
|
:row="row"
|
|
82
129
|
:value="row[header.key]"
|
|
130
|
+
:header="header"
|
|
131
|
+
:row-index="rowIndex"
|
|
132
|
+
:source-index="sourceRowIndex(row, rowIndex)"
|
|
83
133
|
/>
|
|
84
134
|
<slot
|
|
85
135
|
v-else-if="slots.cell"
|
|
86
136
|
name="cell"
|
|
87
137
|
:row="row"
|
|
88
138
|
:value="row[header.key]"
|
|
139
|
+
:header="header"
|
|
140
|
+
:row-index="rowIndex"
|
|
141
|
+
:source-index="sourceRowIndex(row, rowIndex)"
|
|
89
142
|
/>
|
|
90
143
|
<span v-else>
|
|
91
144
|
{{ row[header.key] }}
|
|
@@ -120,9 +173,11 @@
|
|
|
120
173
|
:value="activeFilterValue"
|
|
121
174
|
:rows="props.data"
|
|
122
175
|
:select-options="activeSelectOptions"
|
|
176
|
+
:can-hide="canHideActiveFilterColumn"
|
|
123
177
|
@apply="onFilterApply"
|
|
124
178
|
@clear="onFilterClear"
|
|
125
179
|
@cancel="onFilterCancel"
|
|
180
|
+
@hide="onFilterHideColumn"
|
|
126
181
|
/>
|
|
127
182
|
|
|
128
183
|
<TableColumnOptionsPopover
|
|
@@ -148,15 +203,25 @@
|
|
|
148
203
|
@apply="onColumnOptionsApply"
|
|
149
204
|
@cancel="onColumnOptionsCancel"
|
|
150
205
|
/>
|
|
206
|
+
|
|
207
|
+
<TableRowContextMenu
|
|
208
|
+
v-model:open="rowContextMenuOpen"
|
|
209
|
+
:items="rowContextMenuItems"
|
|
210
|
+
:client-x="rowContextMenuX"
|
|
211
|
+
:client-y="rowContextMenuY"
|
|
212
|
+
:target-count="rowContextMenuTargetKeys.length"
|
|
213
|
+
@select="onRowContextMenuSelect"
|
|
214
|
+
/>
|
|
151
215
|
</template>
|
|
152
216
|
|
|
153
217
|
<script setup>
|
|
154
|
-
import { ref, computed, watch, onMounted, onBeforeUnmount, useSlots, useAttrs } from 'vue'
|
|
218
|
+
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount, useSlots, useAttrs } from 'vue'
|
|
155
219
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
156
220
|
import { LayoutGridIcon } from '@hugeicons/core-free-icons'
|
|
157
221
|
import Pagination from './Pagination.vue'
|
|
158
222
|
import TableColumnFilterPopover from './TableColumnFilterPopover.vue'
|
|
159
223
|
import TableColumnOptionsPopover from './TableColumnOptionsPopover.vue'
|
|
224
|
+
import TableRowContextMenu from './TableRowContextMenu.vue'
|
|
160
225
|
import { useLongPress } from '../composables/useLongPress.js'
|
|
161
226
|
import {
|
|
162
227
|
buildSelectOptions,
|
|
@@ -189,11 +254,6 @@ import {
|
|
|
189
254
|
|
|
190
255
|
const DEFAULT_PAGE_SIZE = 10
|
|
191
256
|
|
|
192
|
-
const sortBy = ref(null)
|
|
193
|
-
const sortDir = ref('asc')
|
|
194
|
-
const page = ref(1)
|
|
195
|
-
const pageSize = ref(DEFAULT_PAGE_SIZE)
|
|
196
|
-
|
|
197
257
|
const props = defineProps({
|
|
198
258
|
headers: {
|
|
199
259
|
type: Array,
|
|
@@ -203,6 +263,29 @@ const props = defineProps({
|
|
|
203
263
|
type: Array,
|
|
204
264
|
default: () => [],
|
|
205
265
|
},
|
|
266
|
+
sortBy: {
|
|
267
|
+
type: [String, Number],
|
|
268
|
+
default: undefined,
|
|
269
|
+
},
|
|
270
|
+
sortDir: {
|
|
271
|
+
type: String,
|
|
272
|
+
default: undefined,
|
|
273
|
+
validator: (value) => value === undefined || value === 'asc' || value === 'desc',
|
|
274
|
+
},
|
|
275
|
+
page: {
|
|
276
|
+
type: Number,
|
|
277
|
+
default: undefined,
|
|
278
|
+
validator: (value) => value === undefined || (Number.isFinite(value) && value >= 1),
|
|
279
|
+
},
|
|
280
|
+
pageSize: {
|
|
281
|
+
type: Number,
|
|
282
|
+
default: undefined,
|
|
283
|
+
validator: (value) => value === undefined || (Number.isFinite(value) && value >= 1),
|
|
284
|
+
},
|
|
285
|
+
resetPageOnSort: {
|
|
286
|
+
type: Boolean,
|
|
287
|
+
default: true,
|
|
288
|
+
},
|
|
206
289
|
showPagination: {
|
|
207
290
|
type: Boolean,
|
|
208
291
|
default: true,
|
|
@@ -276,6 +359,53 @@ const props = defineProps({
|
|
|
276
359
|
type: Object,
|
|
277
360
|
default: null,
|
|
278
361
|
},
|
|
362
|
+
selectable: {
|
|
363
|
+
type: Boolean,
|
|
364
|
+
default: false,
|
|
365
|
+
},
|
|
366
|
+
selectedKeys: {
|
|
367
|
+
type: Array,
|
|
368
|
+
default: undefined,
|
|
369
|
+
},
|
|
370
|
+
selectionClickMode: {
|
|
371
|
+
type: String,
|
|
372
|
+
default: 'row',
|
|
373
|
+
validator: (value) => ['row', 'checkbox', 'modifier', 'extended'].includes(value),
|
|
374
|
+
},
|
|
375
|
+
activeCell: {
|
|
376
|
+
type: Object,
|
|
377
|
+
default: undefined,
|
|
378
|
+
},
|
|
379
|
+
rowClass: {
|
|
380
|
+
type: Function,
|
|
381
|
+
default: null,
|
|
382
|
+
},
|
|
383
|
+
rowStyle: {
|
|
384
|
+
type: Function,
|
|
385
|
+
default: null,
|
|
386
|
+
},
|
|
387
|
+
cellClass: {
|
|
388
|
+
type: Function,
|
|
389
|
+
default: null,
|
|
390
|
+
},
|
|
391
|
+
cellStyle: {
|
|
392
|
+
type: Function,
|
|
393
|
+
default: null,
|
|
394
|
+
},
|
|
395
|
+
cellAttrs: {
|
|
396
|
+
type: Function,
|
|
397
|
+
default: null,
|
|
398
|
+
},
|
|
399
|
+
/**
|
|
400
|
+
* Row right-click menu. Array of items, or a function that returns items
|
|
401
|
+
* for the click context. Each item may include `action(ctx)` where
|
|
402
|
+
* `ctx.keys` / `ctx.rows` are the target selection (clicked row, or all
|
|
403
|
+
* selected rows when the click is inside the current multi-selection).
|
|
404
|
+
*/
|
|
405
|
+
rowContextMenu: {
|
|
406
|
+
type: [Array, Function],
|
|
407
|
+
default: null,
|
|
408
|
+
},
|
|
279
409
|
})
|
|
280
410
|
|
|
281
411
|
const emit = defineEmits([
|
|
@@ -284,6 +414,16 @@ const emit = defineEmits([
|
|
|
284
414
|
'query-change',
|
|
285
415
|
'fetch-error',
|
|
286
416
|
'row-click',
|
|
417
|
+
'cell-click',
|
|
418
|
+
'update:sortBy',
|
|
419
|
+
'update:sortDir',
|
|
420
|
+
'update:page',
|
|
421
|
+
'update:pageSize',
|
|
422
|
+
'update:selectedKeys',
|
|
423
|
+
'selection-change',
|
|
424
|
+
'update:activeCell',
|
|
425
|
+
'row-context-menu',
|
|
426
|
+
'row-context-menu-action',
|
|
287
427
|
'update:columnVisibility',
|
|
288
428
|
'column-visibility-change',
|
|
289
429
|
'update:columnOrder',
|
|
@@ -297,8 +437,111 @@ const emit = defineEmits([
|
|
|
297
437
|
const attrs = useAttrs()
|
|
298
438
|
const slots = useSlots()
|
|
299
439
|
|
|
300
|
-
|
|
440
|
+
function normalizePage(value) {
|
|
441
|
+
const numeric = Number(value)
|
|
442
|
+
return Number.isFinite(numeric) && numeric >= 1 ? Math.floor(numeric) : 1
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function normalizePageSize(value) {
|
|
446
|
+
const numeric = Number(value)
|
|
447
|
+
return Number.isFinite(numeric) && numeric >= 1 ? Math.floor(numeric) : DEFAULT_PAGE_SIZE
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function normalizeSortDir(value) {
|
|
451
|
+
return value === 'desc' ? 'desc' : 'asc'
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function normalizeActiveCell(value) {
|
|
455
|
+
if (
|
|
456
|
+
!value
|
|
457
|
+
|| typeof value !== 'object'
|
|
458
|
+
|| value.rowKey === null
|
|
459
|
+
|| value.rowKey === undefined
|
|
460
|
+
|| value.columnKey === null
|
|
461
|
+
|| value.columnKey === undefined
|
|
462
|
+
) {
|
|
463
|
+
return null
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
rowKey: value.rowKey,
|
|
467
|
+
columnKey: value.columnKey,
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const internalSortBy = ref(props.sortBy !== undefined ? props.sortBy : null)
|
|
472
|
+
const internalSortDir = ref(normalizeSortDir(props.sortDir))
|
|
473
|
+
const internalPage = ref(normalizePage(props.page))
|
|
474
|
+
const internalPageSize = ref(normalizePageSize(props.pageSize))
|
|
475
|
+
const internalActiveCell = ref(normalizeActiveCell(props.activeCell))
|
|
476
|
+
|
|
477
|
+
const sortBy = computed({
|
|
478
|
+
get: () => props.sortBy !== undefined ? props.sortBy : internalSortBy.value,
|
|
479
|
+
set: (value) => {
|
|
480
|
+
const nextValue = value ?? null
|
|
481
|
+
if (nextValue === sortBy.value) {
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
if (props.sortBy === undefined) {
|
|
485
|
+
internalSortBy.value = nextValue
|
|
486
|
+
}
|
|
487
|
+
emit('update:sortBy', nextValue)
|
|
488
|
+
},
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
const sortDir = computed({
|
|
492
|
+
get: () => props.sortDir !== undefined ? normalizeSortDir(props.sortDir) : internalSortDir.value,
|
|
493
|
+
set: (value) => {
|
|
494
|
+
const nextValue = normalizeSortDir(value)
|
|
495
|
+
if (nextValue === sortDir.value) {
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
if (props.sortDir === undefined) {
|
|
499
|
+
internalSortDir.value = nextValue
|
|
500
|
+
}
|
|
501
|
+
emit('update:sortDir', nextValue)
|
|
502
|
+
},
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const page = computed({
|
|
506
|
+
get: () => props.page !== undefined ? normalizePage(props.page) : internalPage.value,
|
|
507
|
+
set: (value) => {
|
|
508
|
+
const nextValue = normalizePage(value)
|
|
509
|
+
if (nextValue === page.value) {
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
if (props.page === undefined) {
|
|
513
|
+
internalPage.value = nextValue
|
|
514
|
+
}
|
|
515
|
+
emit('update:page', nextValue)
|
|
516
|
+
},
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
const pageSize = computed({
|
|
520
|
+
get: () => props.pageSize !== undefined ? normalizePageSize(props.pageSize) : internalPageSize.value,
|
|
521
|
+
set: (value) => {
|
|
522
|
+
const nextValue = normalizePageSize(value)
|
|
523
|
+
if (nextValue === pageSize.value) {
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
if (props.pageSize === undefined) {
|
|
527
|
+
internalPageSize.value = nextValue
|
|
528
|
+
}
|
|
529
|
+
emit('update:pageSize', nextValue)
|
|
530
|
+
},
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
const activeCell = computed(() => (
|
|
534
|
+
props.activeCell !== undefined ? normalizeActiveCell(props.activeCell) : internalActiveCell.value
|
|
535
|
+
))
|
|
536
|
+
|
|
537
|
+
const rowClickable = computed(() =>
|
|
538
|
+
Boolean(attrs.onRowClick)
|
|
539
|
+
|| (props.selectable && props.selectionClickMode !== 'checkbox'),
|
|
540
|
+
)
|
|
301
541
|
const internalFilters = ref({})
|
|
542
|
+
const internalSelectedKeys = ref([])
|
|
543
|
+
const selectionAnchorKey = ref(null)
|
|
544
|
+
const pendingSelectionCheckboxClick = ref(null)
|
|
302
545
|
const remoteRows = ref([])
|
|
303
546
|
const remoteTotal = ref(0)
|
|
304
547
|
const internalLoading = ref(false)
|
|
@@ -491,11 +734,20 @@ const filteredItems = computed(() => {
|
|
|
491
734
|
return applyFilterQuery(props.data, props.headers, activeFilterQuery.value)
|
|
492
735
|
})
|
|
493
736
|
|
|
737
|
+
const activeSortHeader = computed(() =>
|
|
738
|
+
props.headers.find((header) => header?.key === sortBy.value),
|
|
739
|
+
)
|
|
740
|
+
|
|
494
741
|
const sortedItems = computed(() => {
|
|
495
742
|
if (isRemote.value) {
|
|
496
743
|
return filteredItems.value
|
|
497
744
|
}
|
|
498
|
-
return sortRows(
|
|
745
|
+
return sortRows(
|
|
746
|
+
filteredItems.value,
|
|
747
|
+
sortBy.value,
|
|
748
|
+
sortDir.value,
|
|
749
|
+
activeSortHeader.value?.comparator,
|
|
750
|
+
)
|
|
499
751
|
})
|
|
500
752
|
|
|
501
753
|
const displayRows = computed(() => {
|
|
@@ -613,6 +865,60 @@ function cellClasses(header) {
|
|
|
613
865
|
}
|
|
614
866
|
}
|
|
615
867
|
|
|
868
|
+
function sourceRowIndex(row, displayIndex) {
|
|
869
|
+
const sourceRows = isRemote.value ? remoteRows.value : props.data
|
|
870
|
+
const index = sourceRows.indexOf(row)
|
|
871
|
+
return index >= 0 ? index : filteredRowIndex(displayIndex)
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function rowContext(row, rowIndex) {
|
|
875
|
+
return {
|
|
876
|
+
row,
|
|
877
|
+
rowIndex,
|
|
878
|
+
sourceIndex: sourceRowIndex(row, rowIndex),
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function cellContext(row, header, rowIndex) {
|
|
883
|
+
return {
|
|
884
|
+
...rowContext(row, rowIndex),
|
|
885
|
+
value: row?.[header.key],
|
|
886
|
+
header,
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function resolveRowClass(row, rowIndex) {
|
|
891
|
+
return props.rowClass?.(rowContext(row, rowIndex))
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function resolveRowStyle(row, rowIndex) {
|
|
895
|
+
return props.rowStyle?.(rowContext(row, rowIndex))
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function resolveCellClass(row, header, rowIndex) {
|
|
899
|
+
return props.cellClass?.(cellContext(row, header, rowIndex))
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function resolveCellStyle(row, header, rowIndex) {
|
|
903
|
+
return props.cellStyle?.(cellContext(row, header, rowIndex))
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function resolveCellAttrs(row, header, rowIndex) {
|
|
907
|
+
const value = props.cellAttrs?.(cellContext(row, header, rowIndex))
|
|
908
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
909
|
+
return {}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const {
|
|
913
|
+
class: _class,
|
|
914
|
+
style: _style,
|
|
915
|
+
key: _key,
|
|
916
|
+
ref: _ref,
|
|
917
|
+
...safeAttrs
|
|
918
|
+
} = value
|
|
919
|
+
return safeAttrs
|
|
920
|
+
}
|
|
921
|
+
|
|
616
922
|
function resolveRowKey(row, index) {
|
|
617
923
|
if (typeof props.rowKey === 'function') {
|
|
618
924
|
const key = props.rowKey(row, index)
|
|
@@ -632,6 +938,312 @@ function resolveRowKey(row, index) {
|
|
|
632
938
|
return index
|
|
633
939
|
}
|
|
634
940
|
|
|
941
|
+
function activeCellsEqual(left, right) {
|
|
942
|
+
return left?.rowKey === right?.rowKey && left?.columnKey === right?.columnKey
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function setActiveCell(nextCell) {
|
|
946
|
+
const normalized = normalizeActiveCell(nextCell)
|
|
947
|
+
if (activeCellsEqual(normalized, activeCell.value)) {
|
|
948
|
+
return
|
|
949
|
+
}
|
|
950
|
+
if (props.activeCell === undefined) {
|
|
951
|
+
internalActiveCell.value = normalized
|
|
952
|
+
}
|
|
953
|
+
emit('update:activeCell', normalized)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function clearActiveCell() {
|
|
957
|
+
setActiveCell(null)
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function isActiveCell(row, header, rowIndex) {
|
|
961
|
+
const current = activeCell.value
|
|
962
|
+
return Boolean(
|
|
963
|
+
current
|
|
964
|
+
&& current.rowKey === resolveRowKey(row, rowIndex)
|
|
965
|
+
&& current.columnKey === header.key,
|
|
966
|
+
)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const activeCellElements = new Map()
|
|
970
|
+
|
|
971
|
+
function registerCellElement(element, row, header, rowIndex) {
|
|
972
|
+
const rowKey = resolveRowKey(row, rowIndex)
|
|
973
|
+
let rowCells = activeCellElements.get(rowKey)
|
|
974
|
+
|
|
975
|
+
if (!element) {
|
|
976
|
+
rowCells?.delete(header.key)
|
|
977
|
+
if (rowCells?.size === 0) {
|
|
978
|
+
activeCellElements.delete(rowKey)
|
|
979
|
+
}
|
|
980
|
+
return
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (!rowCells) {
|
|
984
|
+
rowCells = new Map()
|
|
985
|
+
activeCellElements.set(rowKey, rowCells)
|
|
986
|
+
}
|
|
987
|
+
rowCells.set(header.key, element)
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function focusCell(cell, options) {
|
|
991
|
+
if (!cell) {
|
|
992
|
+
return false
|
|
993
|
+
}
|
|
994
|
+
const element = activeCellElements.get(cell.rowKey)?.get(cell.columnKey)
|
|
995
|
+
if (!element) {
|
|
996
|
+
return false
|
|
997
|
+
}
|
|
998
|
+
element.focus(options)
|
|
999
|
+
return true
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function focusActiveCell(options) {
|
|
1003
|
+
return focusCell(activeCell.value, options)
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function cellContextAt(rowIndex, columnIndex) {
|
|
1007
|
+
const row = displayRows.value[rowIndex]
|
|
1008
|
+
const header = visibleHeaders.value[columnIndex]
|
|
1009
|
+
if (!row || !header) {
|
|
1010
|
+
return null
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return {
|
|
1014
|
+
...cellContext(row, header, rowIndex),
|
|
1015
|
+
rowKey: resolveRowKey(row, rowIndex),
|
|
1016
|
+
columnKey: header.key,
|
|
1017
|
+
columnIndex,
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function getActiveCellContext() {
|
|
1022
|
+
const current = activeCell.value
|
|
1023
|
+
if (!current) {
|
|
1024
|
+
return null
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const rowIndex = displayRows.value.findIndex(
|
|
1028
|
+
(row, index) => resolveRowKey(row, index) === current.rowKey,
|
|
1029
|
+
)
|
|
1030
|
+
const columnIndex = visibleHeaders.value.findIndex(
|
|
1031
|
+
(header) => header.key === current.columnKey,
|
|
1032
|
+
)
|
|
1033
|
+
if (rowIndex < 0 || columnIndex < 0) {
|
|
1034
|
+
return null
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return cellContextAt(rowIndex, columnIndex)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function clampGridIndex(value, maximum) {
|
|
1041
|
+
const numeric = Number(value)
|
|
1042
|
+
const normalized = Number.isFinite(numeric) ? Math.trunc(numeric) : 0
|
|
1043
|
+
return Math.min(Math.max(normalized, 0), maximum)
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function scheduleCellFocus(cell, options) {
|
|
1047
|
+
if (!options?.focus) {
|
|
1048
|
+
return
|
|
1049
|
+
}
|
|
1050
|
+
void nextTick(() => focusCell(cell, options.focusOptions))
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function activateCellAt(rowIndex, columnIndex, options = {}) {
|
|
1054
|
+
if (displayRows.value.length === 0 || visibleHeaders.value.length === 0) {
|
|
1055
|
+
return null
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const nextRowIndex = clampGridIndex(rowIndex, displayRows.value.length - 1)
|
|
1059
|
+
const nextColumnIndex = clampGridIndex(columnIndex, visibleHeaders.value.length - 1)
|
|
1060
|
+
const context = cellContextAt(nextRowIndex, nextColumnIndex)
|
|
1061
|
+
const nextActiveCell = {
|
|
1062
|
+
rowKey: context.rowKey,
|
|
1063
|
+
columnKey: context.columnKey,
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
setActiveCell(nextActiveCell)
|
|
1067
|
+
scheduleCellFocus(nextActiveCell, options)
|
|
1068
|
+
return context
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function moveActiveCell(rowDelta, columnDelta, options = {}) {
|
|
1072
|
+
if (displayRows.value.length === 0 || visibleHeaders.value.length === 0) {
|
|
1073
|
+
return null
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const current = getActiveCellContext()
|
|
1077
|
+
if (!current) {
|
|
1078
|
+
return activateCellAt(0, 0, options)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const normalizedRowDelta = Number.isFinite(Number(rowDelta)) ? Math.trunc(Number(rowDelta)) : 0
|
|
1082
|
+
const normalizedColumnDelta = Number.isFinite(Number(columnDelta))
|
|
1083
|
+
? Math.trunc(Number(columnDelta))
|
|
1084
|
+
: 0
|
|
1085
|
+
const rowIndex = clampGridIndex(
|
|
1086
|
+
current.rowIndex + normalizedRowDelta,
|
|
1087
|
+
displayRows.value.length - 1,
|
|
1088
|
+
)
|
|
1089
|
+
let columnIndex
|
|
1090
|
+
|
|
1091
|
+
if (options.wrapColumns) {
|
|
1092
|
+
const linearIndex = (rowIndex * visibleHeaders.value.length)
|
|
1093
|
+
+ current.columnIndex
|
|
1094
|
+
+ normalizedColumnDelta
|
|
1095
|
+
const clampedLinearIndex = clampGridIndex(
|
|
1096
|
+
linearIndex,
|
|
1097
|
+
(displayRows.value.length * visibleHeaders.value.length) - 1,
|
|
1098
|
+
)
|
|
1099
|
+
return activateCellAt(
|
|
1100
|
+
Math.floor(clampedLinearIndex / visibleHeaders.value.length),
|
|
1101
|
+
clampedLinearIndex % visibleHeaders.value.length,
|
|
1102
|
+
options,
|
|
1103
|
+
)
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
columnIndex = clampGridIndex(
|
|
1107
|
+
current.columnIndex + normalizedColumnDelta,
|
|
1108
|
+
visibleHeaders.value.length - 1,
|
|
1109
|
+
)
|
|
1110
|
+
return activateCellAt(rowIndex, columnIndex, options)
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function onCellClick(row, header, rowIndex, event) {
|
|
1114
|
+
const context = cellContext(row, header, rowIndex)
|
|
1115
|
+
const nextActiveCell = {
|
|
1116
|
+
rowKey: resolveRowKey(row, rowIndex),
|
|
1117
|
+
columnKey: header.key,
|
|
1118
|
+
}
|
|
1119
|
+
setActiveCell(nextActiveCell)
|
|
1120
|
+
emit('cell-click', {
|
|
1121
|
+
...context,
|
|
1122
|
+
...nextActiveCell,
|
|
1123
|
+
event,
|
|
1124
|
+
})
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const activeSelectedKeys = computed(() => (
|
|
1128
|
+
props.selectedKeys !== undefined ? props.selectedKeys : internalSelectedKeys.value
|
|
1129
|
+
))
|
|
1130
|
+
|
|
1131
|
+
const selectedKeySet = computed(() => new Set(activeSelectedKeys.value))
|
|
1132
|
+
|
|
1133
|
+
function isRowSelected(row, index) {
|
|
1134
|
+
return selectedKeySet.value.has(resolveRowKey(row, index))
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function setSelectedKeys(nextKeys) {
|
|
1138
|
+
const unique = [...new Set(nextKeys)]
|
|
1139
|
+
if (props.selectedKeys === undefined) {
|
|
1140
|
+
internalSelectedKeys.value = unique
|
|
1141
|
+
}
|
|
1142
|
+
emit('update:selectedKeys', unique)
|
|
1143
|
+
emit('selection-change', unique)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function toggleRowSelection(row, index) {
|
|
1147
|
+
const key = resolveRowKey(row, index)
|
|
1148
|
+
const current = activeSelectedKeys.value
|
|
1149
|
+
selectionAnchorKey.value = key
|
|
1150
|
+
if (selectedKeySet.value.has(key)) {
|
|
1151
|
+
setSelectedKeys(current.filter((item) => item !== key))
|
|
1152
|
+
return
|
|
1153
|
+
}
|
|
1154
|
+
setSelectedKeys([...current, key])
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function replaceRowSelection(row, index) {
|
|
1158
|
+
const key = resolveRowKey(row, index)
|
|
1159
|
+
selectionAnchorKey.value = key
|
|
1160
|
+
setSelectedKeys([key])
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const displayRowKeys = computed(() =>
|
|
1164
|
+
displayRows.value.map((row, index) => resolveRowKey(row, index)),
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
function selectRowRange(row, index, { updateAnchor = true } = {}) {
|
|
1168
|
+
const key = resolveRowKey(row, index)
|
|
1169
|
+
const anchorIndex = displayRowKeys.value.findIndex((item) => item === selectionAnchorKey.value)
|
|
1170
|
+
const targetIndex = displayRowKeys.value.findIndex((item) => item === key)
|
|
1171
|
+
|
|
1172
|
+
if (anchorIndex < 0 || targetIndex < 0) {
|
|
1173
|
+
setSelectedKeys([...activeSelectedKeys.value, key])
|
|
1174
|
+
selectionAnchorKey.value = key
|
|
1175
|
+
return
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const from = Math.min(anchorIndex, targetIndex)
|
|
1179
|
+
const to = Math.max(anchorIndex, targetIndex)
|
|
1180
|
+
setSelectedKeys([
|
|
1181
|
+
...activeSelectedKeys.value,
|
|
1182
|
+
...displayRowKeys.value.slice(from, to + 1),
|
|
1183
|
+
])
|
|
1184
|
+
if (updateAnchor) {
|
|
1185
|
+
selectionAnchorKey.value = key
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function rememberSelectionCheckboxClick(row, index, event) {
|
|
1190
|
+
pendingSelectionCheckboxClick.value = {
|
|
1191
|
+
key: resolveRowKey(row, index),
|
|
1192
|
+
shiftKey: event.shiftKey,
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function onSelectionCheckboxChange(row, index) {
|
|
1197
|
+
const key = resolveRowKey(row, index)
|
|
1198
|
+
const shiftKey = (
|
|
1199
|
+
pendingSelectionCheckboxClick.value?.key === key
|
|
1200
|
+
&& pendingSelectionCheckboxClick.value.shiftKey
|
|
1201
|
+
)
|
|
1202
|
+
pendingSelectionCheckboxClick.value = null
|
|
1203
|
+
|
|
1204
|
+
if (shiftKey) {
|
|
1205
|
+
selectRowRange(row, index)
|
|
1206
|
+
return
|
|
1207
|
+
}
|
|
1208
|
+
toggleRowSelection(row, index)
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const allDisplayRowsSelected = computed(() => {
|
|
1212
|
+
const keys = displayRowKeys.value
|
|
1213
|
+
return keys.length > 0 && keys.every((key) => selectedKeySet.value.has(key))
|
|
1214
|
+
})
|
|
1215
|
+
|
|
1216
|
+
const someDisplayRowsSelected = computed(() => {
|
|
1217
|
+
if (allDisplayRowsSelected.value) {
|
|
1218
|
+
return false
|
|
1219
|
+
}
|
|
1220
|
+
return displayRowKeys.value.some((key) => selectedKeySet.value.has(key))
|
|
1221
|
+
})
|
|
1222
|
+
|
|
1223
|
+
function toggleSelectAllDisplayRows() {
|
|
1224
|
+
const pageKeys = displayRowKeys.value
|
|
1225
|
+
if (pageKeys.length === 0) {
|
|
1226
|
+
return
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (allDisplayRowsSelected.value) {
|
|
1230
|
+
const pageKeySet = new Set(pageKeys)
|
|
1231
|
+
setSelectedKeys(activeSelectedKeys.value.filter((key) => !pageKeySet.has(key)))
|
|
1232
|
+
selectionAnchorKey.value = null
|
|
1233
|
+
return
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
setSelectedKeys([...activeSelectedKeys.value, ...pageKeys])
|
|
1237
|
+
selectionAnchorKey.value = pageKeys.at(-1) ?? null
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
watch(() => props.selectable, (enabled) => {
|
|
1241
|
+
if (!enabled) {
|
|
1242
|
+
setSelectedKeys([])
|
|
1243
|
+
selectionAnchorKey.value = null
|
|
1244
|
+
}
|
|
1245
|
+
})
|
|
1246
|
+
|
|
635
1247
|
function setFilters(nextFilters) {
|
|
636
1248
|
const cloned = cloneFilters(nextFilters)
|
|
637
1249
|
if (props.filters === undefined) {
|
|
@@ -686,7 +1298,188 @@ function onRowClick(row, displayIndex, event) {
|
|
|
686
1298
|
if (event.target.closest('a, button, input, select, textarea, [data-row-click-ignore]')) {
|
|
687
1299
|
return
|
|
688
1300
|
}
|
|
689
|
-
|
|
1301
|
+
|
|
1302
|
+
if (props.selectable) {
|
|
1303
|
+
if (props.selectionClickMode === 'extended') {
|
|
1304
|
+
if (event.ctrlKey || event.metaKey) {
|
|
1305
|
+
toggleRowSelection(row, displayIndex)
|
|
1306
|
+
return
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const hasRangeAnchor = displayRowKeys.value.includes(selectionAnchorKey.value)
|
|
1310
|
+
if (event.shiftKey && hasRangeAnchor) {
|
|
1311
|
+
selectRowRange(row, displayIndex, { updateAnchor: false })
|
|
1312
|
+
return
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
replaceRowSelection(row, displayIndex)
|
|
1316
|
+
return
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
if (
|
|
1320
|
+
props.selectionClickMode !== 'checkbox'
|
|
1321
|
+
&& event.shiftKey
|
|
1322
|
+
) {
|
|
1323
|
+
selectRowRange(row, displayIndex)
|
|
1324
|
+
return
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
if (
|
|
1328
|
+
props.selectionClickMode === 'row'
|
|
1329
|
+
|| (
|
|
1330
|
+
props.selectionClickMode === 'modifier'
|
|
1331
|
+
&& (event.ctrlKey || event.metaKey)
|
|
1332
|
+
)
|
|
1333
|
+
) {
|
|
1334
|
+
toggleRowSelection(row, displayIndex)
|
|
1335
|
+
return
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
emit('row-click', {
|
|
1340
|
+
row,
|
|
1341
|
+
index: filteredRowIndex(displayIndex),
|
|
1342
|
+
event,
|
|
1343
|
+
})
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const rowContextMenuOpen = ref(false)
|
|
1347
|
+
const rowContextMenuX = ref(0)
|
|
1348
|
+
const rowContextMenuY = ref(0)
|
|
1349
|
+
const rowContextMenuItems = ref([])
|
|
1350
|
+
const rowContextMenuTargetKeys = ref([])
|
|
1351
|
+
const rowContextMenuAnchor = ref(null)
|
|
1352
|
+
|
|
1353
|
+
const rowContextMenuTargetKeySet = computed(() => new Set(rowContextMenuTargetKeys.value))
|
|
1354
|
+
|
|
1355
|
+
function isRowContextMenuTarget(row, index) {
|
|
1356
|
+
return rowContextMenuOpen.value && rowContextMenuTargetKeySet.value.has(resolveRowKey(row, index))
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const hasRowContextMenu = computed(() => {
|
|
1360
|
+
if (typeof props.rowContextMenu === 'function') {
|
|
1361
|
+
return true
|
|
1362
|
+
}
|
|
1363
|
+
return Array.isArray(props.rowContextMenu) && props.rowContextMenu.length > 0
|
|
1364
|
+
})
|
|
1365
|
+
|
|
1366
|
+
function buildRowLookup() {
|
|
1367
|
+
const map = new Map()
|
|
1368
|
+
const addRows = (rows) => {
|
|
1369
|
+
rows.forEach((row, index) => {
|
|
1370
|
+
const key = resolveRowKey(row, index)
|
|
1371
|
+
if (!map.has(key)) {
|
|
1372
|
+
map.set(key, row)
|
|
1373
|
+
}
|
|
1374
|
+
})
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
if (isRemote.value) {
|
|
1378
|
+
addRows(remoteRows.value)
|
|
1379
|
+
} else {
|
|
1380
|
+
addRows(props.data)
|
|
1381
|
+
}
|
|
1382
|
+
addRows(displayRows.value)
|
|
1383
|
+
return map
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function resolveTargetRows(keys) {
|
|
1387
|
+
const lookup = buildRowLookup()
|
|
1388
|
+
return keys.map((key) => lookup.get(key)).filter((row) => row != null)
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function normalizeContextMenuItems(rawItems) {
|
|
1392
|
+
if (!Array.isArray(rawItems)) {
|
|
1393
|
+
return []
|
|
1394
|
+
}
|
|
1395
|
+
return rawItems.filter((item) => item && (item.divider || item.label || item.id))
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function closeRowContextMenu() {
|
|
1399
|
+
rowContextMenuOpen.value = false
|
|
1400
|
+
rowContextMenuItems.value = []
|
|
1401
|
+
rowContextMenuTargetKeys.value = []
|
|
1402
|
+
rowContextMenuAnchor.value = null
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function onRowContextMenu(row, displayIndex, event) {
|
|
1406
|
+
if (!hasRowContextMenu.value) {
|
|
1407
|
+
return
|
|
1408
|
+
}
|
|
1409
|
+
if (event.target.closest('a, button, input, select, textarea, [data-row-click-ignore]')) {
|
|
1410
|
+
return
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
event.preventDefault()
|
|
1414
|
+
event.stopPropagation()
|
|
1415
|
+
|
|
1416
|
+
const key = resolveRowKey(row, displayIndex)
|
|
1417
|
+
const index = filteredRowIndex(displayIndex)
|
|
1418
|
+
let targetKeys
|
|
1419
|
+
|
|
1420
|
+
if (props.selectable && selectedKeySet.value.has(key) && activeSelectedKeys.value.length > 0) {
|
|
1421
|
+
targetKeys = [...activeSelectedKeys.value]
|
|
1422
|
+
} else {
|
|
1423
|
+
targetKeys = [key]
|
|
1424
|
+
if (props.selectable) {
|
|
1425
|
+
setSelectedKeys([key])
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
const rows = resolveTargetRows(targetKeys)
|
|
1430
|
+
const ctx = {
|
|
1431
|
+
row,
|
|
1432
|
+
key,
|
|
1433
|
+
index,
|
|
1434
|
+
rows,
|
|
1435
|
+
keys: targetKeys,
|
|
1436
|
+
selectedKeys: [...activeSelectedKeys.value],
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const rawItems = typeof props.rowContextMenu === 'function'
|
|
1440
|
+
? props.rowContextMenu(ctx)
|
|
1441
|
+
: props.rowContextMenu
|
|
1442
|
+
const items = normalizeContextMenuItems(rawItems)
|
|
1443
|
+
if (items.length === 0) {
|
|
1444
|
+
closeRowContextMenu()
|
|
1445
|
+
return
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
rowContextMenuAnchor.value = ctx
|
|
1449
|
+
rowContextMenuTargetKeys.value = targetKeys
|
|
1450
|
+
rowContextMenuItems.value = items
|
|
1451
|
+
rowContextMenuX.value = event.clientX
|
|
1452
|
+
rowContextMenuY.value = event.clientY
|
|
1453
|
+
rowContextMenuOpen.value = true
|
|
1454
|
+
|
|
1455
|
+
emit('row-context-menu', {
|
|
1456
|
+
...ctx,
|
|
1457
|
+
event,
|
|
1458
|
+
})
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function onRowContextMenuSelect(item) {
|
|
1462
|
+
const anchor = rowContextMenuAnchor.value
|
|
1463
|
+
const keys = [...rowContextMenuTargetKeys.value]
|
|
1464
|
+
const lookup = buildRowLookup()
|
|
1465
|
+
const rows = keys.map((key) => lookup.get(key)).filter((row) => row != null)
|
|
1466
|
+
const selectedKeys = [...activeSelectedKeys.value]
|
|
1467
|
+
const payload = {
|
|
1468
|
+
item,
|
|
1469
|
+
row: anchor?.row,
|
|
1470
|
+
key: anchor?.key,
|
|
1471
|
+
index: anchor?.index,
|
|
1472
|
+
rows,
|
|
1473
|
+
keys,
|
|
1474
|
+
selectedKeys,
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (typeof item.action === 'function') {
|
|
1478
|
+
item.action(payload)
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
emit('row-context-menu-action', payload)
|
|
1482
|
+
closeRowContextMenu()
|
|
690
1483
|
}
|
|
691
1484
|
|
|
692
1485
|
function openFilter(header, event) {
|
|
@@ -756,6 +1549,39 @@ function onFilterCancel() {
|
|
|
756
1549
|
activeFilterValue.value = []
|
|
757
1550
|
}
|
|
758
1551
|
|
|
1552
|
+
const canHideActiveFilterColumn = computed(() => {
|
|
1553
|
+
if (!showColumnOptionsButton.value || !activeFilterHeader.value) {
|
|
1554
|
+
return false
|
|
1555
|
+
}
|
|
1556
|
+
const header = activeFilterHeader.value
|
|
1557
|
+
if (!header.key || header.hideable === false || header.hidden) {
|
|
1558
|
+
return false
|
|
1559
|
+
}
|
|
1560
|
+
if (!isHeaderHidden(header) && visibleColumnKeys.value.length <= 1) {
|
|
1561
|
+
return false
|
|
1562
|
+
}
|
|
1563
|
+
return configurableHeaders.value.some((item) => item.key === header.key)
|
|
1564
|
+
})
|
|
1565
|
+
|
|
1566
|
+
function onFilterHideColumn() {
|
|
1567
|
+
const header = activeFilterHeader.value
|
|
1568
|
+
if (!header?.key || !canHideActiveFilterColumn.value) {
|
|
1569
|
+
onFilterCancel()
|
|
1570
|
+
return
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
const nextVisibleKeys = visibleColumnKeys.value.filter((key) => key !== header.key)
|
|
1574
|
+
if (nextVisibleKeys.length === 0) {
|
|
1575
|
+
onFilterCancel()
|
|
1576
|
+
return
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
setColumnVisibility(nextVisibleKeys)
|
|
1580
|
+
updateActiveLayoutLabel()
|
|
1581
|
+
filterPopoverOpen.value = false
|
|
1582
|
+
onFilterCancel()
|
|
1583
|
+
}
|
|
1584
|
+
|
|
759
1585
|
function buildColumnVisibility(visibleKeys) {
|
|
760
1586
|
const visibleKeySet = new Set(visibleKeys)
|
|
761
1587
|
return Object.fromEntries(
|
|
@@ -1238,7 +2064,7 @@ function scheduleFetchRows() {
|
|
|
1238
2064
|
}
|
|
1239
2065
|
|
|
1240
2066
|
watch(
|
|
1241
|
-
|
|
2067
|
+
pageSize,
|
|
1242
2068
|
() => {
|
|
1243
2069
|
page.value = 1
|
|
1244
2070
|
if (isRemote.value) {
|
|
@@ -1248,6 +2074,19 @@ watch(
|
|
|
1248
2074
|
},
|
|
1249
2075
|
)
|
|
1250
2076
|
|
|
2077
|
+
watch(
|
|
2078
|
+
() => [sortBy.value, sortDir.value],
|
|
2079
|
+
() => {
|
|
2080
|
+
if (props.resetPageOnSort) {
|
|
2081
|
+
page.value = 1
|
|
2082
|
+
}
|
|
2083
|
+
if (isRemote.value) {
|
|
2084
|
+
scheduleFetchRows()
|
|
2085
|
+
}
|
|
2086
|
+
emitQueryChange()
|
|
2087
|
+
},
|
|
2088
|
+
)
|
|
2089
|
+
|
|
1251
2090
|
watch(page, () => {
|
|
1252
2091
|
if (isRemote.value) {
|
|
1253
2092
|
scheduleFetchRows()
|
|
@@ -1255,6 +2094,40 @@ watch(page, () => {
|
|
|
1255
2094
|
emitQueryChange()
|
|
1256
2095
|
})
|
|
1257
2096
|
|
|
2097
|
+
watch(() => props.sortBy, (value) => {
|
|
2098
|
+
if (value !== undefined) {
|
|
2099
|
+
internalSortBy.value = value ?? null
|
|
2100
|
+
}
|
|
2101
|
+
})
|
|
2102
|
+
|
|
2103
|
+
watch(() => props.sortDir, (value) => {
|
|
2104
|
+
if (value !== undefined) {
|
|
2105
|
+
internalSortDir.value = normalizeSortDir(value)
|
|
2106
|
+
}
|
|
2107
|
+
})
|
|
2108
|
+
|
|
2109
|
+
watch(() => props.page, (value) => {
|
|
2110
|
+
if (value !== undefined) {
|
|
2111
|
+
internalPage.value = normalizePage(value)
|
|
2112
|
+
}
|
|
2113
|
+
})
|
|
2114
|
+
|
|
2115
|
+
watch(() => props.pageSize, (value) => {
|
|
2116
|
+
if (value !== undefined) {
|
|
2117
|
+
internalPageSize.value = normalizePageSize(value)
|
|
2118
|
+
}
|
|
2119
|
+
})
|
|
2120
|
+
|
|
2121
|
+
watch(
|
|
2122
|
+
() => props.activeCell,
|
|
2123
|
+
(value) => {
|
|
2124
|
+
if (value !== undefined) {
|
|
2125
|
+
internalActiveCell.value = normalizeActiveCell(value)
|
|
2126
|
+
}
|
|
2127
|
+
},
|
|
2128
|
+
{ deep: true },
|
|
2129
|
+
)
|
|
2130
|
+
|
|
1258
2131
|
watch(
|
|
1259
2132
|
activeFilters,
|
|
1260
2133
|
() => {
|
|
@@ -1362,11 +2235,15 @@ watch(activeLayoutLabel, (label) => {
|
|
|
1362
2235
|
emitLayoutLabelChange(label)
|
|
1363
2236
|
}, { immediate: true })
|
|
1364
2237
|
|
|
1365
|
-
watch(
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
2238
|
+
watch(
|
|
2239
|
+
() => props.data,
|
|
2240
|
+
() => {
|
|
2241
|
+
if (!isRemote.value) {
|
|
2242
|
+
page.value = 1
|
|
2243
|
+
}
|
|
2244
|
+
},
|
|
2245
|
+
{ deep: true },
|
|
2246
|
+
)
|
|
1370
2247
|
|
|
1371
2248
|
onMounted(async () => {
|
|
1372
2249
|
if (props.loadSavedLayout) {
|
|
@@ -1409,6 +2286,12 @@ onBeforeUnmount(() => {
|
|
|
1409
2286
|
defineExpose({
|
|
1410
2287
|
layoutLabel: activeLayoutLabel,
|
|
1411
2288
|
loadDeveloperDefaults,
|
|
2289
|
+
clearFilters: clearAllFilters,
|
|
2290
|
+
focusActiveCell,
|
|
2291
|
+
clearActiveCell,
|
|
2292
|
+
activateCellAt,
|
|
2293
|
+
moveActiveCell,
|
|
2294
|
+
getActiveCellContext,
|
|
1412
2295
|
})
|
|
1413
2296
|
</script>
|
|
1414
2297
|
|
|
@@ -1457,10 +2340,85 @@ th:first-child {
|
|
|
1457
2340
|
padding-left: 1rem;
|
|
1458
2341
|
}
|
|
1459
2342
|
|
|
2343
|
+
th.table-select-col,
|
|
2344
|
+
td.table-select-col {
|
|
2345
|
+
width: 2.5rem;
|
|
2346
|
+
min-width: 2.5rem;
|
|
2347
|
+
max-width: 2.5rem;
|
|
2348
|
+
text-align: center;
|
|
2349
|
+
vertical-align: middle;
|
|
2350
|
+
padding-left: 1rem;
|
|
2351
|
+
padding-right: 0.25rem;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
.table-select-label {
|
|
2355
|
+
display: inline-flex;
|
|
2356
|
+
align-items: center;
|
|
2357
|
+
justify-content: center;
|
|
2358
|
+
margin: 0;
|
|
2359
|
+
cursor: pointer;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
1460
2362
|
tbody tr.row-clickable {
|
|
1461
2363
|
cursor: pointer;
|
|
1462
2364
|
}
|
|
1463
2365
|
|
|
2366
|
+
tbody td.table-active-cell {
|
|
2367
|
+
outline: 2px solid currentColor;
|
|
2368
|
+
outline-offset: -2px;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
tbody td.table-active-cell:focus,
|
|
2372
|
+
tbody td.table-active-cell:focus-visible {
|
|
2373
|
+
outline-width: 3px;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
tbody tr.row-selected > td {
|
|
2377
|
+
background-color: var(--control-checked-bg);
|
|
2378
|
+
color: var(--control-checked-fg);
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
table.row-hover tbody tr.row-selected:hover > td,
|
|
2382
|
+
table.row-hover tbody tr.row-selected:focus-within > td,
|
|
2383
|
+
table.row-hover tbody tr.row-selected.row-context-menu-active > td {
|
|
2384
|
+
background-color: var(--control-checked-hover-bg);
|
|
2385
|
+
color: var(--control-checked-fg);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
/* Keep hover styling while the row context menu is open (pointer has left the row). */
|
|
2389
|
+
table.row-hover tbody tr.row-context-menu-active > td {
|
|
2390
|
+
background-color: var(--table-row-hover-bg);
|
|
2391
|
+
color: var(--table-row-hover-fg);
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
/* Beat Femtocrank sticky cell backgrounds so selection remains visible while scrolling. */
|
|
2395
|
+
.table-scroll.sticky-cols-1 table tbody tr.row-selected > td,
|
|
2396
|
+
.table-scroll.sticky-cols-2 table tbody tr.row-selected > td,
|
|
2397
|
+
.table-scroll.sticky-cols-3 table tbody tr.row-selected > td {
|
|
2398
|
+
background-color: var(--control-checked-bg);
|
|
2399
|
+
color: var(--control-checked-fg);
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
.table-scroll.sticky-cols-1 table.row-hover tbody tr.row-selected:hover > td,
|
|
2403
|
+
.table-scroll.sticky-cols-1 table.row-hover tbody tr.row-selected:focus-within > td,
|
|
2404
|
+
.table-scroll.sticky-cols-1 table.row-hover tbody tr.row-selected.row-context-menu-active > td,
|
|
2405
|
+
.table-scroll.sticky-cols-2 table.row-hover tbody tr.row-selected:hover > td,
|
|
2406
|
+
.table-scroll.sticky-cols-2 table.row-hover tbody tr.row-selected:focus-within > td,
|
|
2407
|
+
.table-scroll.sticky-cols-2 table.row-hover tbody tr.row-selected.row-context-menu-active > td,
|
|
2408
|
+
.table-scroll.sticky-cols-3 table.row-hover tbody tr.row-selected:hover > td,
|
|
2409
|
+
.table-scroll.sticky-cols-3 table.row-hover tbody tr.row-selected:focus-within > td,
|
|
2410
|
+
.table-scroll.sticky-cols-3 table.row-hover tbody tr.row-selected.row-context-menu-active > td {
|
|
2411
|
+
background-color: var(--control-checked-hover-bg);
|
|
2412
|
+
color: var(--control-checked-fg);
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
.table-scroll.sticky-cols-1 table.row-hover tbody tr.row-context-menu-active > td,
|
|
2416
|
+
.table-scroll.sticky-cols-2 table.row-hover tbody tr.row-context-menu-active > td,
|
|
2417
|
+
.table-scroll.sticky-cols-3 table.row-hover tbody tr.row-context-menu-active > td {
|
|
2418
|
+
background-color: var(--table-row-hover-bg);
|
|
2419
|
+
color: var(--table-row-hover-fg);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
1464
2422
|
.table-empty-state {
|
|
1465
2423
|
display: flex;
|
|
1466
2424
|
align-items: center;
|