picocrank 1.33.0 → 1.35.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/filter-query-v1.md +5 -1
- package/docs/table.md +21 -2
- package/package.json +1 -1
- package/router.js +33 -0
- package/styles/picocrank-extensions.css +23 -2
- package/vue/components/Accordion.spec.js +48 -0
- package/vue/components/Accordion.vue +46 -0
- package/vue/components/AccordionItem.vue +103 -0
- package/vue/components/FieldValue.spec.js +88 -0
- package/vue/components/FieldValue.vue +222 -0
- package/vue/components/FormField.spec.js +1 -0
- package/vue/components/FormField.vue +38 -10
- package/vue/components/Header.vue +24 -2
- package/vue/components/NotificationBlock.vue +25 -0
- package/vue/components/ReorderList.spec.js +318 -0
- package/vue/components/ReorderList.vue +614 -0
- package/vue/components/Table.spec.js +193 -0
- package/vue/components/Table.vue +821 -115
- package/vue/components/TableColumnFilterPopover.vue +63 -19
- package/vue/components/TableColumnOptionsPopover.vue +248 -273
- package/vue/components/TopBar.vue +6 -2
- package/vue/components/accordionContext.js +1 -0
- package/vue/composables/tableColumnPresets.js +33 -0
- package/vue/composables/tableGroupBy.js +173 -0
- package/vue/composables/tableGroupBy.spec.js +69 -0
- package/vue/composables/tableViewSettings.js +34 -0
- package/vue/composables/tableViewSettings.spec.js +21 -0
- package/vue/composables/useHeaderTopBarFit.js +91 -0
- package/vue/composables/useResponsiveNav.js +5 -3
- package/vue/data/navSectionHubs.js +16 -1
- package/vue/examples/AccordionExample.vue +33 -0
- package/vue/examples/FieldValueExample.vue +143 -0
- package/vue/examples/FormExample.vue +11 -6
- package/vue/examples/ReorderListExample.vue +236 -0
- package/vue/examples/TableExample.vue +5 -1
- package/vue/examples/TableRemoteExample.vue +28 -4
package/vue/components/Table.vue
CHANGED
|
@@ -4,11 +4,31 @@
|
|
|
4
4
|
:class="needsTableWrapper ? tableWrapperClasses : undefined"
|
|
5
5
|
:style="needsTableWrapper ? undefined : { display: 'contents' }"
|
|
6
6
|
>
|
|
7
|
-
<table
|
|
7
|
+
<table
|
|
8
|
+
ref="tableElementRef"
|
|
9
|
+
class="row-hover"
|
|
10
|
+
:class="{
|
|
11
|
+
loading: isLoading,
|
|
12
|
+
'has-column-widths': hasResizedColumns,
|
|
13
|
+
'is-column-resizing': activeResizeKey != null,
|
|
14
|
+
}"
|
|
15
|
+
:aria-busy="isLoading ? 'true' : undefined"
|
|
16
|
+
:aria-colcount="ariaColCount"
|
|
17
|
+
>
|
|
18
|
+
<colgroup>
|
|
19
|
+
<col v-if="selectable" class="table-select-col-width" />
|
|
20
|
+
<col
|
|
21
|
+
v-for="(header, index) in visibleHeaders"
|
|
22
|
+
:key="header.key || index"
|
|
23
|
+
:style="columnWidthStyle(header)"
|
|
24
|
+
/>
|
|
25
|
+
<col v-if="showColumnOptionsButton" class="table-column-options-col-width" />
|
|
26
|
+
</colgroup>
|
|
8
27
|
<thead>
|
|
9
28
|
<tr>
|
|
10
29
|
<th
|
|
11
30
|
v-if="selectable"
|
|
31
|
+
scope="col"
|
|
12
32
|
class="table-select-col"
|
|
13
33
|
@click.stop
|
|
14
34
|
>
|
|
@@ -25,8 +45,13 @@
|
|
|
25
45
|
<th
|
|
26
46
|
v-for="(header, index) in visibleHeaders"
|
|
27
47
|
:key="header.key || index"
|
|
28
|
-
|
|
29
|
-
:
|
|
48
|
+
scope="col"
|
|
49
|
+
:data-column-key="header.key"
|
|
50
|
+
:class="[headerClasses(header), header.class, { 'is-resizing': activeResizeKey === header.key }]"
|
|
51
|
+
:style="[columnWidthStyle(header), columnOverflowStyle()]"
|
|
52
|
+
:aria-sort="headerAriaSort(header)"
|
|
53
|
+
:aria-haspopup="isHeaderFilterable(header) ? 'dialog' : undefined"
|
|
54
|
+
:aria-expanded="headerFilterExpanded(header)"
|
|
30
55
|
@click="onHeaderClick(header, $event)"
|
|
31
56
|
@contextmenu.prevent="openFilter(header, $event)"
|
|
32
57
|
@pointerdown="onHeaderPointerDown(header, $event)"
|
|
@@ -41,15 +66,58 @@
|
|
|
41
66
|
|
|
42
67
|
<span v-if="filterable && isHeaderFiltered(header)" class="filter-indicator" title="Filtered" aria-hidden="true">⧩</span>
|
|
43
68
|
|
|
44
|
-
<span
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
69
|
+
<span
|
|
70
|
+
v-if="isHeaderGrouped(header)"
|
|
71
|
+
class="group-indicator"
|
|
72
|
+
title="Grouped"
|
|
73
|
+
aria-hidden="true"
|
|
74
|
+
>
|
|
75
|
+
<HugeiconsIcon
|
|
76
|
+
:icon="Group01Icon"
|
|
77
|
+
width="0.95em"
|
|
78
|
+
height="0.95em"
|
|
79
|
+
:strokeWidth="2"
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
/>
|
|
82
|
+
</span>
|
|
83
|
+
|
|
84
|
+
<span v-if="header.sortable" class="sort-indicator" aria-hidden="true">
|
|
85
|
+
<HugeiconsIcon
|
|
86
|
+
v-if="sortBy === header.key && sortDir === 'asc'"
|
|
87
|
+
:icon="SortByUp02Icon"
|
|
88
|
+
width="0.95em"
|
|
89
|
+
height="0.95em"
|
|
90
|
+
:strokeWidth="2"
|
|
91
|
+
aria-hidden="true"
|
|
92
|
+
/>
|
|
93
|
+
<HugeiconsIcon
|
|
94
|
+
v-else-if="sortBy === header.key && sortDir === 'desc'"
|
|
95
|
+
:icon="SortByDown02Icon"
|
|
96
|
+
width="0.95em"
|
|
97
|
+
height="0.95em"
|
|
98
|
+
:strokeWidth="2"
|
|
99
|
+
aria-hidden="true"
|
|
100
|
+
/>
|
|
49
101
|
</span>
|
|
102
|
+
|
|
103
|
+
<span
|
|
104
|
+
v-if="isHeaderResizable(header)"
|
|
105
|
+
class="column-resize-handle"
|
|
106
|
+
role="separator"
|
|
107
|
+
aria-orientation="vertical"
|
|
108
|
+
:aria-label="`Resize ${header.label || header.key} column`"
|
|
109
|
+
:aria-valuemin="MIN_COLUMN_WIDTH"
|
|
110
|
+
:aria-valuenow="columnResizeValueNow(header)"
|
|
111
|
+
:class="{ 'is-active': activeResizeKey === header.key }"
|
|
112
|
+
@pointerdown.stop="onResizePointerDown(header, $event)"
|
|
113
|
+
@dblclick.stop="onResizeDoubleClick(header, $event)"
|
|
114
|
+
@click.stop
|
|
115
|
+
@contextmenu.stop.prevent
|
|
116
|
+
/>
|
|
50
117
|
</th>
|
|
51
118
|
<th
|
|
52
119
|
v-if="showColumnOptionsButton"
|
|
120
|
+
scope="col"
|
|
53
121
|
class="actions table-column-options-header"
|
|
54
122
|
>
|
|
55
123
|
<span class="actions-menu-trigger">
|
|
@@ -74,77 +142,113 @@
|
|
|
74
142
|
</tr>
|
|
75
143
|
</thead>
|
|
76
144
|
<tbody v-if="hasRows">
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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)"
|
|
89
|
-
@click="onRowClick(row, rowIndex, $event)"
|
|
90
|
-
@contextmenu="onRowContextMenu(row, rowIndex, $event)"
|
|
91
|
-
>
|
|
92
|
-
<td
|
|
93
|
-
v-if="selectable"
|
|
94
|
-
class="table-select-col"
|
|
95
|
-
data-row-click-ignore
|
|
96
|
-
@click.stop
|
|
145
|
+
<template v-for="(item, itemIndex) in displayBodyItems" :key="bodyItemKey(item, itemIndex)">
|
|
146
|
+
<tr
|
|
147
|
+
v-if="item.kind === 'group'"
|
|
148
|
+
class="table-group-row"
|
|
149
|
+
:class="{ 'table-group-row-collapsed': item.collapsed }"
|
|
97
150
|
>
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
151
|
+
<td
|
|
152
|
+
v-if="selectable"
|
|
153
|
+
class="table-select-col table-group-cell"
|
|
154
|
+
data-row-click-ignore
|
|
155
|
+
/>
|
|
156
|
+
<td
|
|
157
|
+
:colspan="bodyGroupColspan"
|
|
158
|
+
class="table-group-cell"
|
|
159
|
+
>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
class="table-group-toggle neutral"
|
|
163
|
+
:aria-expanded="item.collapsed ? 'false' : 'true'"
|
|
164
|
+
:aria-label="item.collapsed ? 'Expand group' : 'Collapse group'"
|
|
165
|
+
data-row-click-ignore
|
|
166
|
+
@click.stop="toggleGroupCollapsed(item.groupId)"
|
|
105
167
|
>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
168
|
+
<HugeiconsIcon
|
|
169
|
+
class="table-group-chevron"
|
|
170
|
+
:icon="ArrowDown01Icon"
|
|
171
|
+
width="1em"
|
|
172
|
+
height="1em"
|
|
173
|
+
aria-hidden="true"
|
|
174
|
+
/>
|
|
175
|
+
</button>
|
|
176
|
+
<span class="table-group-label">{{ formatGroupValue(item.value) }}</span>
|
|
177
|
+
<span class="table-group-count subtle">({{ item.count }})</span>
|
|
178
|
+
</td>
|
|
179
|
+
</tr>
|
|
180
|
+
<tr
|
|
181
|
+
v-else
|
|
114
182
|
:class="[
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
183
|
+
{
|
|
184
|
+
'row-clickable': rowClickable,
|
|
185
|
+
'row-selected': selectable && isRowSelected(item.row, item.displayIndex),
|
|
186
|
+
'row-context-menu-active': isRowContextMenuTarget(item.row, item.displayIndex),
|
|
187
|
+
},
|
|
188
|
+
resolveRowClass(item.row, item.displayIndex),
|
|
120
189
|
]"
|
|
121
|
-
:style="
|
|
122
|
-
|
|
123
|
-
@
|
|
190
|
+
:style="resolveRowStyle(item.row, item.displayIndex)"
|
|
191
|
+
@click="onRowClick(item.row, item.displayIndex, $event)"
|
|
192
|
+
@contextmenu="onRowContextMenu(item.row, item.displayIndex, $event)"
|
|
124
193
|
>
|
|
125
|
-
<
|
|
126
|
-
v-if="
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
194
|
+
<td
|
|
195
|
+
v-if="selectable"
|
|
196
|
+
class="table-select-col"
|
|
197
|
+
data-row-click-ignore
|
|
198
|
+
@click.stop
|
|
199
|
+
>
|
|
200
|
+
<label class="table-select-label">
|
|
201
|
+
<input
|
|
202
|
+
type="checkbox"
|
|
203
|
+
:checked="isRowSelected(item.row, item.displayIndex)"
|
|
204
|
+
:aria-label="`Select row ${resolveRowKey(item.row, item.displayIndex)}`"
|
|
205
|
+
@click.stop="rememberSelectionCheckboxClick(item.row, item.displayIndex, $event)"
|
|
206
|
+
@change="onSelectionCheckboxChange(item.row, item.displayIndex)"
|
|
207
|
+
>
|
|
208
|
+
</label>
|
|
209
|
+
</td>
|
|
210
|
+
<td
|
|
211
|
+
v-for="(header, cellIndex) in visibleHeaders"
|
|
212
|
+
:key="header.key || cellIndex"
|
|
213
|
+
:ref="(element) => registerCellElement(element, item.row, header, item.displayIndex)"
|
|
214
|
+
v-bind="resolveCellAttrs(item.row, header, item.displayIndex)"
|
|
215
|
+
:colspan="bodyColumnColspan(cellIndex) || undefined"
|
|
216
|
+
:class="[
|
|
217
|
+
cellClasses(header),
|
|
218
|
+
header.class,
|
|
219
|
+
resolveCellClass(item.row, header, item.displayIndex),
|
|
220
|
+
{ 'table-active-cell': isActiveCell(item.row, header, item.displayIndex) },
|
|
221
|
+
isLastColumnWithOptions(cellIndex) ? 'table-last-column-with-options' : null,
|
|
222
|
+
]"
|
|
223
|
+
:style="[resolveCellStyle(item.row, header, item.displayIndex), columnOverflowStyle()]"
|
|
224
|
+
:tabindex="isActiveCell(item.row, header, item.displayIndex) ? 0 : -1"
|
|
225
|
+
@click="onCellClick(item.row, header, item.displayIndex, $event)"
|
|
226
|
+
@keydown="onCellKeydown(item.row, header, item.displayIndex, $event)"
|
|
227
|
+
>
|
|
228
|
+
<slot
|
|
229
|
+
v-if="slots[`cell-${header.key}`]"
|
|
230
|
+
:name="`cell-${header.key}`"
|
|
231
|
+
:row="item.row"
|
|
232
|
+
:value="item.row[header.key]"
|
|
233
|
+
:header="header"
|
|
234
|
+
:row-index="item.displayIndex"
|
|
235
|
+
:source-index="sourceRowIndex(item.row, item.displayIndex)"
|
|
236
|
+
/>
|
|
237
|
+
<slot
|
|
238
|
+
v-else-if="slots.cell"
|
|
239
|
+
name="cell"
|
|
240
|
+
:row="item.row"
|
|
241
|
+
:value="item.row[header.key]"
|
|
242
|
+
:header="header"
|
|
243
|
+
:row-index="item.displayIndex"
|
|
244
|
+
:source-index="sourceRowIndex(item.row, item.displayIndex)"
|
|
245
|
+
/>
|
|
246
|
+
<span v-else>
|
|
247
|
+
{{ item.row[header.key] }}
|
|
248
|
+
</span>
|
|
249
|
+
</td>
|
|
250
|
+
</tr>
|
|
251
|
+
</template>
|
|
148
252
|
</tbody>
|
|
149
253
|
</table>
|
|
150
254
|
|
|
@@ -174,10 +278,13 @@
|
|
|
174
278
|
:rows="props.data"
|
|
175
279
|
:select-options="activeSelectOptions"
|
|
176
280
|
:can-hide="canHideActiveFilterColumn"
|
|
281
|
+
:can-group-by="canGroupByActiveFilterColumn"
|
|
282
|
+
:is-grouped-column="isActiveFilterHeaderGrouped"
|
|
177
283
|
@apply="onFilterApply"
|
|
178
284
|
@clear="onFilterClear"
|
|
179
285
|
@cancel="onFilterCancel"
|
|
180
286
|
@hide="onFilterHideColumn"
|
|
287
|
+
@group-by="onFilterGroupBy"
|
|
181
288
|
/>
|
|
182
289
|
|
|
183
290
|
<TableColumnOptionsPopover
|
|
@@ -195,10 +302,13 @@
|
|
|
195
302
|
:presets="savedPresets"
|
|
196
303
|
:default-preset-id="defaultPresetId"
|
|
197
304
|
:save-layout-preset="onSavePreset"
|
|
305
|
+
:overwrite-layout-preset="onOverwritePreset"
|
|
198
306
|
:load-layout-preset="onLoadPreset"
|
|
199
307
|
:delete-layout-preset="onDeletePreset"
|
|
200
308
|
:set-default-layout-preset="onSetDefaultPreset"
|
|
201
309
|
:load-developer-defaults="loadDeveloperDefaults"
|
|
310
|
+
:group-by="activeGroupBy"
|
|
311
|
+
:groupable="props.groupable"
|
|
202
312
|
:anchor-el="columnOptionsButtonRef"
|
|
203
313
|
@apply="onColumnOptionsApply"
|
|
204
314
|
@cancel="onColumnOptionsCancel"
|
|
@@ -217,7 +327,13 @@
|
|
|
217
327
|
<script setup>
|
|
218
328
|
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount, useSlots, useAttrs } from 'vue'
|
|
219
329
|
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
220
|
-
import {
|
|
330
|
+
import {
|
|
331
|
+
ArrowDown01Icon,
|
|
332
|
+
Group01Icon,
|
|
333
|
+
LayoutGridIcon,
|
|
334
|
+
SortByDown02Icon,
|
|
335
|
+
SortByUp02Icon,
|
|
336
|
+
} from '@hugeicons/core-free-icons'
|
|
221
337
|
import Pagination from './Pagination.vue'
|
|
222
338
|
import TableColumnFilterPopover from './TableColumnFilterPopover.vue'
|
|
223
339
|
import TableColumnOptionsPopover from './TableColumnOptionsPopover.vue'
|
|
@@ -242,17 +358,29 @@ import {
|
|
|
242
358
|
getPresetById,
|
|
243
359
|
listPresets,
|
|
244
360
|
savePreset,
|
|
361
|
+
overwritePreset,
|
|
245
362
|
deletePreset,
|
|
246
363
|
setDefaultPreset,
|
|
247
364
|
sanitizePresetState,
|
|
248
365
|
layoutStatesEqual,
|
|
249
366
|
} from '../composables/tableColumnPresets.js'
|
|
367
|
+
import {
|
|
368
|
+
getViewSettingsHandlers,
|
|
369
|
+
isViewSettingsControlEnabled,
|
|
370
|
+
VIEW_CONTROL_CALLBACK,
|
|
371
|
+
} from '../composables/tableViewSettings.js'
|
|
250
372
|
import {
|
|
251
373
|
defaultColumnPriorityForIndex,
|
|
252
374
|
isValidColPriority,
|
|
253
375
|
} from '../composables/tableColumnPriorities.js'
|
|
376
|
+
import {
|
|
377
|
+
buildDisplayBodyItems,
|
|
378
|
+
formatGroupValue,
|
|
379
|
+
isHeaderGroupable,
|
|
380
|
+
} from '../composables/tableGroupBy.js'
|
|
254
381
|
|
|
255
382
|
const DEFAULT_PAGE_SIZE = 10
|
|
383
|
+
const MIN_COLUMN_WIDTH = 48
|
|
256
384
|
|
|
257
385
|
const props = defineProps({
|
|
258
386
|
headers: {
|
|
@@ -343,6 +471,21 @@ const props = defineProps({
|
|
|
343
471
|
type: String,
|
|
344
472
|
default: '',
|
|
345
473
|
},
|
|
474
|
+
/**
|
|
475
|
+
* How saved table views (column order, visibility, filters in presets, etc.) are stored.
|
|
476
|
+
* - `localStorageControlled`: requires `tableId`; uses browser localStorage (default).
|
|
477
|
+
* - `callbackControlled`: requires `viewSettings` callbacks (server or app-owned persistence).
|
|
478
|
+
*/
|
|
479
|
+
viewControlMode: {
|
|
480
|
+
type: String,
|
|
481
|
+
default: 'localStorageControlled',
|
|
482
|
+
validator: (value) => ['localStorageControlled', 'callbackControlled'].includes(value),
|
|
483
|
+
},
|
|
484
|
+
/** Callback handlers when `viewControlMode` is `callbackControlled`. */
|
|
485
|
+
viewSettings: {
|
|
486
|
+
type: Object,
|
|
487
|
+
default: null,
|
|
488
|
+
},
|
|
346
489
|
loadSavedLayout: {
|
|
347
490
|
type: Boolean,
|
|
348
491
|
default: true,
|
|
@@ -355,6 +498,7 @@ const props = defineProps({
|
|
|
355
498
|
type: [String, Function],
|
|
356
499
|
default: 'id',
|
|
357
500
|
},
|
|
501
|
+
/** @deprecated Use `viewSettings` with `viewControlMode="callbackControlled"`. */
|
|
358
502
|
layoutPresets: {
|
|
359
503
|
type: Object,
|
|
360
504
|
default: null,
|
|
@@ -406,6 +550,14 @@ const props = defineProps({
|
|
|
406
550
|
type: [Array, Function],
|
|
407
551
|
default: null,
|
|
408
552
|
},
|
|
553
|
+
groupable: {
|
|
554
|
+
type: Boolean,
|
|
555
|
+
default: true,
|
|
556
|
+
},
|
|
557
|
+
groupBy: {
|
|
558
|
+
type: String,
|
|
559
|
+
default: undefined,
|
|
560
|
+
},
|
|
409
561
|
})
|
|
410
562
|
|
|
411
563
|
const emit = defineEmits([
|
|
@@ -432,6 +584,8 @@ const emit = defineEmits([
|
|
|
432
584
|
'column-priorities-change',
|
|
433
585
|
'update:layoutLabel',
|
|
434
586
|
'layout-label-change',
|
|
587
|
+
'update:groupBy',
|
|
588
|
+
'group-by-change',
|
|
435
589
|
])
|
|
436
590
|
|
|
437
591
|
const attrs = useAttrs()
|
|
@@ -561,15 +715,192 @@ const internalColumnPriorities = ref({})
|
|
|
561
715
|
const savedPresets = ref([])
|
|
562
716
|
const defaultPresetId = ref(null)
|
|
563
717
|
const activeLayoutPresetId = ref(null)
|
|
718
|
+
const internalGroupBy = ref(null)
|
|
719
|
+
const collapsedGroupIds = ref([])
|
|
720
|
+
/** @type {import('vue').Ref<Record<string, number | null>>} */
|
|
721
|
+
const columnWidths = ref({})
|
|
722
|
+
const activeResizeKey = ref(null)
|
|
723
|
+
const tableElementRef = ref(null)
|
|
724
|
+
let resizeSession = null
|
|
564
725
|
|
|
565
726
|
const isRemote = computed(() => typeof props.fetchRows === 'function')
|
|
566
727
|
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
728
|
+
const hasResizedColumns = computed(() =>
|
|
729
|
+
Object.values(columnWidths.value).some((width) => width != null),
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
function isHeaderResizable(header) {
|
|
733
|
+
return Boolean(header?.key) && header.resizable !== false
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function formatColumnWidth(width) {
|
|
737
|
+
if (width == null || width === '') {
|
|
738
|
+
return undefined
|
|
570
739
|
}
|
|
571
|
-
return
|
|
572
|
-
}
|
|
740
|
+
return typeof width === 'number' ? `${width}px` : width
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function columnWidthStyle(header) {
|
|
744
|
+
if (!header?.key) {
|
|
745
|
+
return header?.width ? { width: formatColumnWidth(header.width) } : undefined
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (Object.prototype.hasOwnProperty.call(columnWidths.value, header.key)) {
|
|
749
|
+
const resized = columnWidths.value[header.key]
|
|
750
|
+
if (resized == null) {
|
|
751
|
+
return undefined
|
|
752
|
+
}
|
|
753
|
+
return { width: `${resized}px` }
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (header.width) {
|
|
757
|
+
return { width: formatColumnWidth(header.width) }
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return undefined
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function columnOverflowStyle() {
|
|
764
|
+
if (!hasResizedColumns.value) {
|
|
765
|
+
return undefined
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
overflow: 'hidden',
|
|
769
|
+
textOverflow: 'ellipsis',
|
|
770
|
+
whiteSpace: 'nowrap',
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function columnResizeValueNow(header) {
|
|
775
|
+
const width = columnWidths.value[header.key]
|
|
776
|
+
return typeof width === 'number' ? width : undefined
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function setColumnWidths(nextWidths) {
|
|
780
|
+
columnWidths.value = nextWidths
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function setColumnWidth(columnKey, width) {
|
|
784
|
+
columnWidths.value = {
|
|
785
|
+
...columnWidths.value,
|
|
786
|
+
[columnKey]: width,
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function freezeSiblingColumnWidths(tableEl, activeKey, activeWidth) {
|
|
791
|
+
const next = { ...columnWidths.value }
|
|
792
|
+
tableEl?.querySelectorAll('thead th[data-column-key]').forEach((th) => {
|
|
793
|
+
const key = th.dataset.columnKey
|
|
794
|
+
if (!key || key === activeKey) {
|
|
795
|
+
return
|
|
796
|
+
}
|
|
797
|
+
if (typeof next[key] === 'number') {
|
|
798
|
+
return
|
|
799
|
+
}
|
|
800
|
+
next[key] = Math.max(MIN_COLUMN_WIDTH, Math.round(th.getBoundingClientRect().width))
|
|
801
|
+
})
|
|
802
|
+
next[activeKey] = activeWidth
|
|
803
|
+
setColumnWidths(next)
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function endColumnResize() {
|
|
807
|
+
if (!resizeSession) {
|
|
808
|
+
return
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const { moveListener, upListener, previousUserSelect, previousCursor } = resizeSession
|
|
812
|
+
window.removeEventListener('pointermove', moveListener)
|
|
813
|
+
window.removeEventListener('pointerup', upListener)
|
|
814
|
+
window.removeEventListener('pointercancel', upListener)
|
|
815
|
+
document.body.style.userSelect = previousUserSelect
|
|
816
|
+
document.body.style.cursor = previousCursor
|
|
817
|
+
activeResizeKey.value = null
|
|
818
|
+
resizeSession = null
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function onResizePointerDown(header, event) {
|
|
822
|
+
if (!isHeaderResizable(header) || event.button !== 0) {
|
|
823
|
+
return
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const handle = event.currentTarget
|
|
827
|
+
const headerCell = handle?.closest('th')
|
|
828
|
+
if (!headerCell) {
|
|
829
|
+
return
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
event.preventDefault()
|
|
833
|
+
endColumnResize()
|
|
834
|
+
|
|
835
|
+
const startX = event.clientX
|
|
836
|
+
const startWidth = Math.max(MIN_COLUMN_WIDTH, Math.round(headerCell.getBoundingClientRect().width))
|
|
837
|
+
activeResizeKey.value = header.key
|
|
838
|
+
freezeSiblingColumnWidths(
|
|
839
|
+
tableElementRef.value ?? headerCell.closest('table'),
|
|
840
|
+
header.key,
|
|
841
|
+
startWidth,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
const previousUserSelect = document.body.style.userSelect
|
|
845
|
+
const previousCursor = document.body.style.cursor
|
|
846
|
+
document.body.style.userSelect = 'none'
|
|
847
|
+
document.body.style.cursor = 'col-resize'
|
|
848
|
+
|
|
849
|
+
const moveListener = (moveEvent) => {
|
|
850
|
+
const nextWidth = Math.max(
|
|
851
|
+
MIN_COLUMN_WIDTH,
|
|
852
|
+
Math.round(startWidth + (moveEvent.clientX - startX)),
|
|
853
|
+
)
|
|
854
|
+
setColumnWidth(header.key, nextWidth)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const upListener = () => {
|
|
858
|
+
endColumnResize()
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
resizeSession = {
|
|
862
|
+
moveListener,
|
|
863
|
+
upListener,
|
|
864
|
+
previousUserSelect,
|
|
865
|
+
previousCursor,
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
window.addEventListener('pointermove', moveListener)
|
|
869
|
+
window.addEventListener('pointerup', upListener)
|
|
870
|
+
window.addEventListener('pointercancel', upListener)
|
|
871
|
+
|
|
872
|
+
if (typeof handle.setPointerCapture === 'function') {
|
|
873
|
+
try {
|
|
874
|
+
handle.setPointerCapture(event.pointerId)
|
|
875
|
+
} catch {
|
|
876
|
+
// Ignore capture failures (e.g. unsupported pointer type in tests).
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function onResizeDoubleClick(header, event) {
|
|
882
|
+
if (!isHeaderResizable(header)) {
|
|
883
|
+
return
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
event.preventDefault()
|
|
887
|
+
endColumnResize()
|
|
888
|
+
setColumnWidth(header.key, null)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const viewSettingsHandlers = computed(() =>
|
|
892
|
+
getViewSettingsHandlers(props.viewSettings, props.layoutPresets))
|
|
893
|
+
|
|
894
|
+
const isCallbackViewControl = computed(
|
|
895
|
+
() => props.viewControlMode === VIEW_CONTROL_CALLBACK,
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
const layoutPresetsAvailable = computed(() =>
|
|
899
|
+
isViewSettingsControlEnabled(
|
|
900
|
+
props.viewControlMode,
|
|
901
|
+
props.tableId,
|
|
902
|
+
viewSettingsHandlers.value,
|
|
903
|
+
))
|
|
573
904
|
|
|
574
905
|
const activeFilters = computed(() => {
|
|
575
906
|
if (!props.filterable) {
|
|
@@ -629,6 +960,53 @@ function bodyColumnColspan(index) {
|
|
|
629
960
|
return isLastColumnWithOptions(index) ? 2 : null
|
|
630
961
|
}
|
|
631
962
|
|
|
963
|
+
const bodyGroupColspan = computed(() => {
|
|
964
|
+
let colspan = visibleHeaders.value.length
|
|
965
|
+
if (showColumnOptionsButton.value) {
|
|
966
|
+
colspan += 1
|
|
967
|
+
}
|
|
968
|
+
return colspan
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
function bodyItemKey(item, index) {
|
|
972
|
+
if (item.kind === 'group') {
|
|
973
|
+
return `group-${item.groupId}`
|
|
974
|
+
}
|
|
975
|
+
return resolveRowKey(item.row, item.sourceIndex ?? index)
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function isHeaderGrouped(header) {
|
|
979
|
+
return Boolean(activeGroupBy.value && header?.key === activeGroupBy.value)
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function toggleGroupCollapsed(groupId) {
|
|
983
|
+
const next = new Set(collapsedGroupIdSet.value)
|
|
984
|
+
if (next.has(groupId)) {
|
|
985
|
+
next.delete(groupId)
|
|
986
|
+
} else {
|
|
987
|
+
next.add(groupId)
|
|
988
|
+
}
|
|
989
|
+
collapsedGroupIds.value = [...next]
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function setGroupBy(nextGroupBy) {
|
|
993
|
+
const header = props.headers.find((item) => item.key === nextGroupBy)
|
|
994
|
+
const normalized = isHeaderGroupable(header, props.groupable) ? nextGroupBy : null
|
|
995
|
+
|
|
996
|
+
if (props.groupBy === undefined) {
|
|
997
|
+
internalGroupBy.value = normalized
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
collapsedGroupIds.value = []
|
|
1001
|
+
page.value = 1
|
|
1002
|
+
emit('update:groupBy', normalized)
|
|
1003
|
+
emit('group-by-change', normalized)
|
|
1004
|
+
if (isRemote.value) {
|
|
1005
|
+
scheduleFetchRows()
|
|
1006
|
+
}
|
|
1007
|
+
emitQueryChange()
|
|
1008
|
+
}
|
|
1009
|
+
|
|
632
1010
|
function isHeaderHidden(header) {
|
|
633
1011
|
if (header.hidden) {
|
|
634
1012
|
return true
|
|
@@ -641,6 +1019,17 @@ function isHeaderHidden(header) {
|
|
|
641
1019
|
|
|
642
1020
|
const visibleHeaders = computed(() => orderedHeaders.value.filter((header) => !isHeaderHidden(header)))
|
|
643
1021
|
|
|
1022
|
+
const ariaColCount = computed(() => {
|
|
1023
|
+
let count = visibleHeaders.value.length
|
|
1024
|
+
if (props.selectable) {
|
|
1025
|
+
count += 1
|
|
1026
|
+
}
|
|
1027
|
+
if (showColumnOptionsButton.value) {
|
|
1028
|
+
count += 1
|
|
1029
|
+
}
|
|
1030
|
+
return count > 0 ? count : undefined
|
|
1031
|
+
})
|
|
1032
|
+
|
|
644
1033
|
const visibleColumnKeys = computed(() =>
|
|
645
1034
|
orderedHeaders.value
|
|
646
1035
|
.filter((header) => !isHeaderHidden(header))
|
|
@@ -750,16 +1139,45 @@ const sortedItems = computed(() => {
|
|
|
750
1139
|
)
|
|
751
1140
|
})
|
|
752
1141
|
|
|
753
|
-
const
|
|
754
|
-
if (
|
|
755
|
-
return
|
|
1142
|
+
const activeGroupBy = computed(() => {
|
|
1143
|
+
if (!props.groupable) {
|
|
1144
|
+
return null
|
|
756
1145
|
}
|
|
1146
|
+
if (props.groupBy !== undefined) {
|
|
1147
|
+
return props.groupBy || null
|
|
1148
|
+
}
|
|
1149
|
+
return internalGroupBy.value
|
|
1150
|
+
})
|
|
1151
|
+
|
|
1152
|
+
const collapsedGroupIdSet = computed(() => new Set(collapsedGroupIds.value))
|
|
757
1153
|
|
|
758
|
-
|
|
759
|
-
|
|
1154
|
+
const displayBodyItems = computed(() => {
|
|
1155
|
+
const paginate = !isRemote.value && props.showPagination
|
|
1156
|
+
const items = buildDisplayBodyItems(sortedItems.value, {
|
|
1157
|
+
groupBy: activeGroupBy.value,
|
|
1158
|
+
page: page.value,
|
|
1159
|
+
pageSize: pageSize.value,
|
|
1160
|
+
paginate,
|
|
1161
|
+
collapsedGroupIds: collapsedGroupIdSet.value,
|
|
1162
|
+
})
|
|
1163
|
+
let displayIndex = 0
|
|
1164
|
+
return items.map((item) => {
|
|
1165
|
+
if (item.kind !== 'row') {
|
|
1166
|
+
return item
|
|
1167
|
+
}
|
|
1168
|
+
const indexed = { ...item, displayIndex }
|
|
1169
|
+
displayIndex += 1
|
|
1170
|
+
return indexed
|
|
1171
|
+
})
|
|
760
1172
|
})
|
|
761
1173
|
|
|
762
|
-
const
|
|
1174
|
+
const displayRows = computed(() =>
|
|
1175
|
+
displayBodyItems.value
|
|
1176
|
+
.filter((item) => item.kind === 'row')
|
|
1177
|
+
.map((item) => item.row),
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
const hasRows = computed(() => displayBodyItems.value.length > 0)
|
|
763
1181
|
|
|
764
1182
|
const hasActiveFilters = computed(() =>
|
|
765
1183
|
isFilterQueryActive(activeFilterQuery.value, props.headers, props.data),
|
|
@@ -813,6 +1231,7 @@ const queryParams = computed(() => {
|
|
|
813
1231
|
pageSize: pageSize.value,
|
|
814
1232
|
sortBy: sortBy.value,
|
|
815
1233
|
sortDir: sortDir.value,
|
|
1234
|
+
groupBy: activeGroupBy.value,
|
|
816
1235
|
filterQuery: activeFilterQuery.value,
|
|
817
1236
|
}
|
|
818
1237
|
|
|
@@ -846,12 +1265,32 @@ function isHeaderFiltered(header) {
|
|
|
846
1265
|
return isColumnFiltered(activeFilters.value?.[header.key], header, props.data)
|
|
847
1266
|
}
|
|
848
1267
|
|
|
1268
|
+
function headerAriaSort(header) {
|
|
1269
|
+
if (!header?.sortable) {
|
|
1270
|
+
return undefined
|
|
1271
|
+
}
|
|
1272
|
+
if (sortBy.value !== header.key) {
|
|
1273
|
+
return 'none'
|
|
1274
|
+
}
|
|
1275
|
+
return sortDir.value === 'desc' ? 'descending' : 'ascending'
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function headerFilterExpanded(header) {
|
|
1279
|
+
if (!isHeaderFilterable(header)) {
|
|
1280
|
+
return undefined
|
|
1281
|
+
}
|
|
1282
|
+
return filterPopoverOpen.value && activeFilterHeader.value?.key === header.key
|
|
1283
|
+
? 'true'
|
|
1284
|
+
: 'false'
|
|
1285
|
+
}
|
|
1286
|
+
|
|
849
1287
|
function headerClasses(header) {
|
|
850
1288
|
const priority = resolvedColumnPriority(header)
|
|
851
1289
|
return {
|
|
852
1290
|
sortable: header.sortable,
|
|
853
1291
|
filterable: isHeaderFilterable(header),
|
|
854
1292
|
filtered: isHeaderFiltered(header),
|
|
1293
|
+
grouped: isHeaderGrouped(header),
|
|
855
1294
|
'menu-open': filterPopoverOpen.value && activeFilterHeader.value?.key === header.key,
|
|
856
1295
|
[colPriorityClass(priority)]: isValidColPriority(priority),
|
|
857
1296
|
}
|
|
@@ -1110,6 +1549,14 @@ function moveActiveCell(rowDelta, columnDelta, options = {}) {
|
|
|
1110
1549
|
return activateCellAt(rowIndex, columnIndex, options)
|
|
1111
1550
|
}
|
|
1112
1551
|
|
|
1552
|
+
function isInteractiveCellTarget(target) {
|
|
1553
|
+
return Boolean(
|
|
1554
|
+
target?.closest?.(
|
|
1555
|
+
'a, button, input, select, textarea, [contenteditable="true"], [data-row-click-ignore]',
|
|
1556
|
+
),
|
|
1557
|
+
)
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1113
1560
|
function onCellClick(row, header, rowIndex, event) {
|
|
1114
1561
|
const context = cellContext(row, header, rowIndex)
|
|
1115
1562
|
const nextActiveCell = {
|
|
@@ -1117,6 +1564,9 @@ function onCellClick(row, header, rowIndex, event) {
|
|
|
1117
1564
|
columnKey: header.key,
|
|
1118
1565
|
}
|
|
1119
1566
|
setActiveCell(nextActiveCell)
|
|
1567
|
+
if (!isInteractiveCellTarget(event.target)) {
|
|
1568
|
+
scheduleCellFocus(nextActiveCell, { focus: true })
|
|
1569
|
+
}
|
|
1120
1570
|
emit('cell-click', {
|
|
1121
1571
|
...context,
|
|
1122
1572
|
...nextActiveCell,
|
|
@@ -1124,6 +1574,39 @@ function onCellClick(row, header, rowIndex, event) {
|
|
|
1124
1574
|
})
|
|
1125
1575
|
}
|
|
1126
1576
|
|
|
1577
|
+
function onCellKeydown(row, header, rowIndex, event) {
|
|
1578
|
+
if (!activeCell.value || !isActiveCell(row, header, rowIndex)) {
|
|
1579
|
+
return
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (isInteractiveCellTarget(event.target) && event.target !== event.currentTarget) {
|
|
1583
|
+
return
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
let rowDelta = 0
|
|
1587
|
+
let columnDelta = 0
|
|
1588
|
+
|
|
1589
|
+
switch (event.key) {
|
|
1590
|
+
case 'ArrowUp':
|
|
1591
|
+
rowDelta = -1
|
|
1592
|
+
break
|
|
1593
|
+
case 'ArrowDown':
|
|
1594
|
+
rowDelta = 1
|
|
1595
|
+
break
|
|
1596
|
+
case 'ArrowLeft':
|
|
1597
|
+
columnDelta = -1
|
|
1598
|
+
break
|
|
1599
|
+
case 'ArrowRight':
|
|
1600
|
+
columnDelta = 1
|
|
1601
|
+
break
|
|
1602
|
+
default:
|
|
1603
|
+
return
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
event.preventDefault()
|
|
1607
|
+
moveActiveCell(rowDelta, columnDelta, { focus: true })
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1127
1610
|
const activeSelectedKeys = computed(() => (
|
|
1128
1611
|
props.selectedKeys !== undefined ? props.selectedKeys : internalSelectedKeys.value
|
|
1129
1612
|
))
|
|
@@ -1402,7 +1885,7 @@ function closeRowContextMenu() {
|
|
|
1402
1885
|
rowContextMenuAnchor.value = null
|
|
1403
1886
|
}
|
|
1404
1887
|
|
|
1405
|
-
function onRowContextMenu(row,
|
|
1888
|
+
function onRowContextMenu(row, sourceIndex, event) {
|
|
1406
1889
|
if (!hasRowContextMenu.value) {
|
|
1407
1890
|
return
|
|
1408
1891
|
}
|
|
@@ -1413,8 +1896,8 @@ function onRowContextMenu(row, displayIndex, event) {
|
|
|
1413
1896
|
event.preventDefault()
|
|
1414
1897
|
event.stopPropagation()
|
|
1415
1898
|
|
|
1416
|
-
const key = resolveRowKey(row,
|
|
1417
|
-
const index = filteredRowIndex(
|
|
1899
|
+
const key = resolveRowKey(row, sourceIndex)
|
|
1900
|
+
const index = filteredRowIndex(sourceIndex)
|
|
1418
1901
|
let targetKeys
|
|
1419
1902
|
|
|
1420
1903
|
if (props.selectable && selectedKeySet.value.has(key) && activeSelectedKeys.value.length > 0) {
|
|
@@ -1549,6 +2032,17 @@ function onFilterCancel() {
|
|
|
1549
2032
|
activeFilterValue.value = []
|
|
1550
2033
|
}
|
|
1551
2034
|
|
|
2035
|
+
const canGroupByActiveFilterColumn = computed(() => {
|
|
2036
|
+
if (!props.groupable || !activeFilterHeader.value) {
|
|
2037
|
+
return false
|
|
2038
|
+
}
|
|
2039
|
+
return isHeaderGroupable(activeFilterHeader.value, props.groupable)
|
|
2040
|
+
})
|
|
2041
|
+
|
|
2042
|
+
const isActiveFilterHeaderGrouped = computed(() =>
|
|
2043
|
+
Boolean(activeFilterHeader.value?.key && activeFilterHeader.value.key === activeGroupBy.value),
|
|
2044
|
+
)
|
|
2045
|
+
|
|
1552
2046
|
const canHideActiveFilterColumn = computed(() => {
|
|
1553
2047
|
if (!showColumnOptionsButton.value || !activeFilterHeader.value) {
|
|
1554
2048
|
return false
|
|
@@ -1563,6 +2057,12 @@ const canHideActiveFilterColumn = computed(() => {
|
|
|
1563
2057
|
return configurableHeaders.value.some((item) => item.key === header.key)
|
|
1564
2058
|
})
|
|
1565
2059
|
|
|
2060
|
+
function onFilterGroupBy(columnKey) {
|
|
2061
|
+
setGroupBy(columnKey || null)
|
|
2062
|
+
filterPopoverOpen.value = false
|
|
2063
|
+
onFilterCancel()
|
|
2064
|
+
}
|
|
2065
|
+
|
|
1566
2066
|
function onFilterHideColumn() {
|
|
1567
2067
|
const header = activeFilterHeader.value
|
|
1568
2068
|
if (!header?.key || !canHideActiveFilterColumn.value) {
|
|
@@ -1648,10 +2148,13 @@ function openColumnOptions() {
|
|
|
1648
2148
|
void refreshSavedPresets()
|
|
1649
2149
|
}
|
|
1650
2150
|
|
|
1651
|
-
function onColumnOptionsApply({ order, visibleKeys, priorities }) {
|
|
2151
|
+
function onColumnOptionsApply({ order, visibleKeys, priorities, groupBy }) {
|
|
1652
2152
|
setColumnOrder(order)
|
|
1653
2153
|
setColumnVisibility(visibleKeys)
|
|
1654
2154
|
setColumnPriorities(priorities)
|
|
2155
|
+
if (groupBy !== undefined && groupBy !== activeGroupBy.value) {
|
|
2156
|
+
setGroupBy(groupBy)
|
|
2157
|
+
}
|
|
1655
2158
|
updateActiveLayoutLabel()
|
|
1656
2159
|
}
|
|
1657
2160
|
|
|
@@ -1660,15 +2163,16 @@ function onColumnOptionsCancel() {
|
|
|
1660
2163
|
}
|
|
1661
2164
|
|
|
1662
2165
|
async function refreshSavedPresets() {
|
|
1663
|
-
if (
|
|
1664
|
-
|
|
2166
|
+
if (isCallbackViewControl.value) {
|
|
2167
|
+
const handlers = viewSettingsHandlers.value
|
|
2168
|
+
if (!handlers || typeof handlers.list !== 'function') {
|
|
1665
2169
|
savedPresets.value = []
|
|
1666
2170
|
defaultPresetId.value = null
|
|
1667
2171
|
return
|
|
1668
2172
|
}
|
|
1669
2173
|
|
|
1670
2174
|
try {
|
|
1671
|
-
const result = await
|
|
2175
|
+
const result = await handlers.list()
|
|
1672
2176
|
savedPresets.value = Array.isArray(result?.presets) ? result.presets : []
|
|
1673
2177
|
defaultPresetId.value = result?.defaultPresetId ?? null
|
|
1674
2178
|
} catch {
|
|
@@ -1706,6 +2210,7 @@ function getCurrentLayoutState() {
|
|
|
1706
2210
|
filters: cloneFilters(activeFilters.value),
|
|
1707
2211
|
sortBy: sortBy.value,
|
|
1708
2212
|
sortDir: sortDir.value,
|
|
2213
|
+
groupBy: activeGroupBy.value,
|
|
1709
2214
|
pageSize: pageSize.value,
|
|
1710
2215
|
}
|
|
1711
2216
|
}
|
|
@@ -1745,6 +2250,12 @@ function applyPresetState(state) {
|
|
|
1745
2250
|
sortDir.value = 'asc'
|
|
1746
2251
|
}
|
|
1747
2252
|
|
|
2253
|
+
if (sanitized.groupBy) {
|
|
2254
|
+
setGroupBy(sanitized.groupBy)
|
|
2255
|
+
} else {
|
|
2256
|
+
setGroupBy(null)
|
|
2257
|
+
}
|
|
2258
|
+
|
|
1748
2259
|
if (sanitized.pageSize) {
|
|
1749
2260
|
pageSize.value = sanitized.pageSize
|
|
1750
2261
|
}
|
|
@@ -1760,13 +2271,14 @@ function applyPresetState(state) {
|
|
|
1760
2271
|
async function onSavePreset({ name, setAsDefault }) {
|
|
1761
2272
|
const state = getCurrentLayoutState()
|
|
1762
2273
|
|
|
1763
|
-
if (
|
|
1764
|
-
|
|
1765
|
-
|
|
2274
|
+
if (isCallbackViewControl.value) {
|
|
2275
|
+
const handlers = viewSettingsHandlers.value
|
|
2276
|
+
if (!handlers || typeof handlers.save !== 'function') {
|
|
2277
|
+
return { ok: false, error: 'View save callback is unavailable.' }
|
|
1766
2278
|
}
|
|
1767
2279
|
|
|
1768
2280
|
try {
|
|
1769
|
-
const result = await
|
|
2281
|
+
const result = await handlers.save({ name, state, setAsDefault })
|
|
1770
2282
|
if (result?.ok) {
|
|
1771
2283
|
await refreshSavedPresets()
|
|
1772
2284
|
if (result.preset?.id) {
|
|
@@ -1799,14 +2311,50 @@ async function onSavePreset({ name, setAsDefault }) {
|
|
|
1799
2311
|
return result
|
|
1800
2312
|
}
|
|
1801
2313
|
|
|
2314
|
+
async function onOverwritePreset({ presetId }) {
|
|
2315
|
+
const state = getCurrentLayoutState()
|
|
2316
|
+
|
|
2317
|
+
if (isCallbackViewControl.value) {
|
|
2318
|
+
const handlers = viewSettingsHandlers.value
|
|
2319
|
+
if (!handlers || typeof handlers.overwrite !== 'function') {
|
|
2320
|
+
return { ok: false, error: 'View overwrite callback is unavailable.' }
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
try {
|
|
2324
|
+
const result = await handlers.overwrite({ presetId, state })
|
|
2325
|
+
if (result?.ok) {
|
|
2326
|
+
await refreshSavedPresets()
|
|
2327
|
+
activeLayoutPresetId.value = presetId
|
|
2328
|
+
updateActiveLayoutLabel()
|
|
2329
|
+
}
|
|
2330
|
+
return result ?? { ok: false, error: 'Unable to overwrite layout.' }
|
|
2331
|
+
} catch {
|
|
2332
|
+
return { ok: false, error: 'Unable to overwrite layout.' }
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
if (!props.tableId) {
|
|
2337
|
+
return { ok: false, error: 'Table identity is required.' }
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
const result = overwritePreset(props.tableId, presetId, { state })
|
|
2341
|
+
if (result.ok) {
|
|
2342
|
+
refreshSavedPresets()
|
|
2343
|
+
activeLayoutPresetId.value = presetId
|
|
2344
|
+
updateActiveLayoutLabel()
|
|
2345
|
+
}
|
|
2346
|
+
return result
|
|
2347
|
+
}
|
|
2348
|
+
|
|
1802
2349
|
async function onLoadPreset(presetId) {
|
|
1803
|
-
if (
|
|
1804
|
-
|
|
1805
|
-
|
|
2350
|
+
if (isCallbackViewControl.value) {
|
|
2351
|
+
const handlers = viewSettingsHandlers.value
|
|
2352
|
+
if (!handlers || typeof handlers.load !== 'function') {
|
|
2353
|
+
return { ok: false, error: 'View load callback is unavailable.' }
|
|
1806
2354
|
}
|
|
1807
2355
|
|
|
1808
2356
|
try {
|
|
1809
|
-
const result = await
|
|
2357
|
+
const result = await handlers.load(presetId)
|
|
1810
2358
|
if (!result?.ok || !result.preset?.state) {
|
|
1811
2359
|
return result ?? { ok: false, error: 'Preset not found.' }
|
|
1812
2360
|
}
|
|
@@ -1836,13 +2384,14 @@ async function onLoadPreset(presetId) {
|
|
|
1836
2384
|
}
|
|
1837
2385
|
|
|
1838
2386
|
async function onDeletePreset(presetId) {
|
|
1839
|
-
if (
|
|
1840
|
-
|
|
1841
|
-
|
|
2387
|
+
if (isCallbackViewControl.value) {
|
|
2388
|
+
const handlers = viewSettingsHandlers.value
|
|
2389
|
+
if (!handlers || typeof handlers.delete !== 'function') {
|
|
2390
|
+
return { ok: false, error: 'View delete callback is unavailable.' }
|
|
1842
2391
|
}
|
|
1843
2392
|
|
|
1844
2393
|
try {
|
|
1845
|
-
const result = await
|
|
2394
|
+
const result = await handlers.delete(presetId)
|
|
1846
2395
|
if (result?.ok) {
|
|
1847
2396
|
await refreshSavedPresets()
|
|
1848
2397
|
if (activeLayoutPresetId.value === presetId) {
|
|
@@ -1872,13 +2421,14 @@ async function onDeletePreset(presetId) {
|
|
|
1872
2421
|
}
|
|
1873
2422
|
|
|
1874
2423
|
async function onSetDefaultPreset(presetId) {
|
|
1875
|
-
if (
|
|
1876
|
-
|
|
2424
|
+
if (isCallbackViewControl.value) {
|
|
2425
|
+
const handlers = viewSettingsHandlers.value
|
|
2426
|
+
if (!handlers || typeof handlers.setDefault !== 'function') {
|
|
1877
2427
|
return { ok: false, error: 'Unable to update default layout.' }
|
|
1878
2428
|
}
|
|
1879
2429
|
|
|
1880
2430
|
try {
|
|
1881
|
-
const result = await
|
|
2431
|
+
const result = await handlers.setDefault(presetId)
|
|
1882
2432
|
if (result?.ok) {
|
|
1883
2433
|
await refreshSavedPresets()
|
|
1884
2434
|
}
|
|
@@ -1921,6 +2471,7 @@ function getDeveloperDefaultLayoutState() {
|
|
|
1921
2471
|
filters: {},
|
|
1922
2472
|
sortBy: null,
|
|
1923
2473
|
sortDir: 'asc',
|
|
2474
|
+
groupBy: null,
|
|
1924
2475
|
pageSize: DEFAULT_PAGE_SIZE,
|
|
1925
2476
|
}
|
|
1926
2477
|
}
|
|
@@ -1980,6 +2531,7 @@ function loadDeveloperDefaults() {
|
|
|
1980
2531
|
setFilters({})
|
|
1981
2532
|
sortBy.value = null
|
|
1982
2533
|
sortDir.value = 'asc'
|
|
2534
|
+
setGroupBy(null)
|
|
1983
2535
|
setColumnOrder(getDeveloperDefaultColumnOrder())
|
|
1984
2536
|
setColumnPriorities({})
|
|
1985
2537
|
|
|
@@ -2074,6 +2626,17 @@ watch(
|
|
|
2074
2626
|
},
|
|
2075
2627
|
)
|
|
2076
2628
|
|
|
2629
|
+
watch(
|
|
2630
|
+
activeGroupBy,
|
|
2631
|
+
() => {
|
|
2632
|
+
page.value = 1
|
|
2633
|
+
if (isRemote.value) {
|
|
2634
|
+
scheduleFetchRows()
|
|
2635
|
+
}
|
|
2636
|
+
emitQueryChange()
|
|
2637
|
+
},
|
|
2638
|
+
)
|
|
2639
|
+
|
|
2077
2640
|
watch(
|
|
2078
2641
|
() => [sortBy.value, sortDir.value],
|
|
2079
2642
|
() => {
|
|
@@ -2184,6 +2747,24 @@ watch(
|
|
|
2184
2747
|
{ deep: true },
|
|
2185
2748
|
)
|
|
2186
2749
|
|
|
2750
|
+
watch(
|
|
2751
|
+
() => props.groupBy,
|
|
2752
|
+
(nextGroupBy) => {
|
|
2753
|
+
if (nextGroupBy !== undefined) {
|
|
2754
|
+
collapsedGroupIds.value = []
|
|
2755
|
+
}
|
|
2756
|
+
},
|
|
2757
|
+
)
|
|
2758
|
+
|
|
2759
|
+
watch(
|
|
2760
|
+
() => props.groupable,
|
|
2761
|
+
(enabled) => {
|
|
2762
|
+
if (!enabled) {
|
|
2763
|
+
setGroupBy(null)
|
|
2764
|
+
}
|
|
2765
|
+
},
|
|
2766
|
+
)
|
|
2767
|
+
|
|
2187
2768
|
watch(
|
|
2188
2769
|
() => props.columnPriorities,
|
|
2189
2770
|
(nextPriorities) => {
|
|
@@ -2204,7 +2785,7 @@ watch(
|
|
|
2204
2785
|
)
|
|
2205
2786
|
|
|
2206
2787
|
watch(
|
|
2207
|
-
() => [props.
|
|
2788
|
+
() => [props.viewControlMode, props.tableId, viewSettingsHandlers.value],
|
|
2208
2789
|
() => {
|
|
2209
2790
|
void refreshSavedPresets()
|
|
2210
2791
|
updateActiveLayoutLabel()
|
|
@@ -2221,6 +2802,7 @@ watch(
|
|
|
2221
2802
|
activeFilters,
|
|
2222
2803
|
sortBy,
|
|
2223
2804
|
sortDir,
|
|
2805
|
+
activeGroupBy,
|
|
2224
2806
|
pageSize,
|
|
2225
2807
|
() => props.headers,
|
|
2226
2808
|
savedPresets,
|
|
@@ -2247,17 +2829,18 @@ watch(
|
|
|
2247
2829
|
|
|
2248
2830
|
onMounted(async () => {
|
|
2249
2831
|
if (props.loadSavedLayout) {
|
|
2250
|
-
|
|
2832
|
+
const handlers = viewSettingsHandlers.value
|
|
2833
|
+
if (isCallbackViewControl.value && handlers && typeof handlers.getDefault === 'function') {
|
|
2251
2834
|
try {
|
|
2252
|
-
const result = await
|
|
2835
|
+
const result = await handlers.getDefault()
|
|
2253
2836
|
if (result?.preset?.state) {
|
|
2254
2837
|
applyPresetState(result.preset.state)
|
|
2255
2838
|
activeLayoutPresetId.value = result.preset.id
|
|
2256
2839
|
}
|
|
2257
2840
|
} catch {
|
|
2258
|
-
// Ignore default layout load failures in
|
|
2841
|
+
// Ignore default layout load failures in callback mode.
|
|
2259
2842
|
}
|
|
2260
|
-
} else if (!
|
|
2843
|
+
} else if (!isCallbackViewControl.value && props.tableId) {
|
|
2261
2844
|
const preset = getDefaultPreset(props.tableId)
|
|
2262
2845
|
if (preset?.state) {
|
|
2263
2846
|
applyPresetState(preset.state)
|
|
@@ -2275,6 +2858,7 @@ onMounted(async () => {
|
|
|
2275
2858
|
})
|
|
2276
2859
|
|
|
2277
2860
|
onBeforeUnmount(() => {
|
|
2861
|
+
endColumnResize()
|
|
2278
2862
|
if (fetchAbortController.value) {
|
|
2279
2863
|
fetchAbortController.value.abort()
|
|
2280
2864
|
}
|
|
@@ -2296,11 +2880,85 @@ defineExpose({
|
|
|
2296
2880
|
</script>
|
|
2297
2881
|
|
|
2298
2882
|
<style scoped>
|
|
2883
|
+
table.has-column-widths {
|
|
2884
|
+
table-layout: fixed;
|
|
2885
|
+
width: 100%;
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
/* Force fixed-layout cells to respect column widths so overflow clipping/ellipsis work. */
|
|
2889
|
+
table.has-column-widths th,
|
|
2890
|
+
table.has-column-widths td {
|
|
2891
|
+
max-width: 0;
|
|
2892
|
+
overflow: hidden;
|
|
2893
|
+
text-overflow: ellipsis;
|
|
2894
|
+
white-space: nowrap;
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
table.has-column-widths th.table-select-col,
|
|
2898
|
+
table.has-column-widths td.table-select-col,
|
|
2899
|
+
table.has-column-widths th.table-column-options-header {
|
|
2900
|
+
max-width: none;
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
table.is-column-resizing {
|
|
2904
|
+
cursor: col-resize;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
table.is-column-resizing thead th.sortable:hover {
|
|
2908
|
+
cursor: col-resize;
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
col.table-select-col-width,
|
|
2912
|
+
col.table-column-options-col-width {
|
|
2913
|
+
width: 2.5rem;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
table thead th {
|
|
2917
|
+
position: relative;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2299
2920
|
table thead th.sortable:hover {
|
|
2300
2921
|
cursor: pointer;
|
|
2301
2922
|
color: var(--table-header-active-fg);
|
|
2302
2923
|
}
|
|
2303
2924
|
|
|
2925
|
+
.column-resize-handle {
|
|
2926
|
+
position: absolute;
|
|
2927
|
+
top: 0;
|
|
2928
|
+
right: -0.2rem;
|
|
2929
|
+
z-index: 2;
|
|
2930
|
+
width: 0.55rem;
|
|
2931
|
+
height: 100%;
|
|
2932
|
+
padding: 0;
|
|
2933
|
+
border: 0;
|
|
2934
|
+
background: transparent;
|
|
2935
|
+
cursor: col-resize;
|
|
2936
|
+
touch-action: none;
|
|
2937
|
+
user-select: none;
|
|
2938
|
+
opacity: 0;
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
.column-resize-handle::after {
|
|
2942
|
+
content: '';
|
|
2943
|
+
position: absolute;
|
|
2944
|
+
top: 20%;
|
|
2945
|
+
bottom: 20%;
|
|
2946
|
+
left: 50%;
|
|
2947
|
+
width: 2px;
|
|
2948
|
+
transform: translateX(-50%);
|
|
2949
|
+
border-radius: 1px;
|
|
2950
|
+
background-color: var(--border-color, currentColor);
|
|
2951
|
+
opacity: 0.85;
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
table thead th:hover .column-resize-handle,
|
|
2955
|
+
table thead th.is-resizing .column-resize-handle,
|
|
2956
|
+
.column-resize-handle:hover,
|
|
2957
|
+
.column-resize-handle:focus-visible,
|
|
2958
|
+
.column-resize-handle.is-active {
|
|
2959
|
+
opacity: 1;
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2304
2962
|
table thead th.filterable {
|
|
2305
2963
|
cursor: context-menu;
|
|
2306
2964
|
}
|
|
@@ -2330,9 +2988,57 @@ table.loading tbody {
|
|
|
2330
2988
|
}
|
|
2331
2989
|
|
|
2332
2990
|
.sort-indicator {
|
|
2991
|
+
display: inline-flex;
|
|
2992
|
+
align-items: center;
|
|
2993
|
+
justify-content: center;
|
|
2333
2994
|
width: 1.5em;
|
|
2334
|
-
|
|
2335
|
-
|
|
2995
|
+
vertical-align: middle;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
.group-indicator {
|
|
2999
|
+
display: inline-flex;
|
|
3000
|
+
vertical-align: middle;
|
|
3001
|
+
margin-right: 0.25rem;
|
|
3002
|
+
opacity: 0.85;
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
table thead th.grouped {
|
|
3006
|
+
color: var(--table-header-active-fg);
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
tbody tr.table-group-row > td.table-group-cell {
|
|
3010
|
+
background-color: var(--table-header-active-bg);
|
|
3011
|
+
color: var(--table-header-fg);
|
|
3012
|
+
font-weight: 600;
|
|
3013
|
+
padding-top: 0.65rem;
|
|
3014
|
+
padding-bottom: 0.65rem;
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
.table-group-toggle {
|
|
3018
|
+
display: inline-flex;
|
|
3019
|
+
align-items: center;
|
|
3020
|
+
justify-content: center;
|
|
3021
|
+
min-width: 1.5rem;
|
|
3022
|
+
margin-right: 0.35rem;
|
|
3023
|
+
padding: 0.1rem 0.35rem;
|
|
3024
|
+
vertical-align: middle;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
.table-group-chevron {
|
|
3028
|
+
flex-shrink: 0;
|
|
3029
|
+
transition: transform 0.15s ease;
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
.table-group-row:not(.table-group-row-collapsed) .table-group-chevron {
|
|
3033
|
+
transform: rotate(180deg);
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
.table-group-label {
|
|
3037
|
+
margin-right: 0.35rem;
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
.table-group-count {
|
|
3041
|
+
font-weight: normal;
|
|
2336
3042
|
}
|
|
2337
3043
|
|
|
2338
3044
|
td:first-child,
|