@@ -7,6 +7,8 @@ import { BAlert, BButton, BCard, BFormCheckbox, BOverlay, BPagination } from "bo
77import { computed , nextTick , onMounted , onUnmounted , ref , watch } from " vue" ;
88import { useRouter } from " vue-router/composables" ;
99
10+ import { useSelectedItems } from " @/composables/selectedItems/selectedItems" ;
11+
1012import type { BatchOperation , FieldEntry , FieldHandler , GridConfig , Operation , RowData } from " ./configs/types" ;
1113
1214import HelpText from " ../Help/HelpText.vue" ;
@@ -67,11 +69,6 @@ const errorMessage = ref("");
6769const operationMessage = ref (" " );
6870const operationStatus = ref (" " );
6971
70- // selection references
71- const selected = ref (new Set <RowData >());
72- const selectedAll = computed (() => gridData .value .length === selected .value .size );
73- const selectedIndeterminate = computed (() => ! [0 , gridData .value .length ].includes (selected .value .size ));
74-
7572// expand references
7673const expanded = ref (new Set <RowData >());
7774
@@ -109,6 +106,54 @@ const hideMessage = useDebounceFn(() => {
109106 operationMessage .value = " " ;
110107}, props .delay );
111108
109+ // selection references
110+ const {
111+ allSelected,
112+ initSelectedItem,
113+ itemRefs,
114+ selectedItems : selected,
115+ isSelected,
116+ onClick,
117+ onKeyDown,
118+ resetSelection,
119+ selectAllInCurrentQuery,
120+ selectionSize,
121+ setSelected,
122+ } = useSelectedItems <RowData , typeof HTMLTableRowElement >({
123+ scopeKey: computed (() => ` grid-${props .gridConfig .id } ` ),
124+ getItemKey: props .gridConfig .getItemKey || getItemKey ,
125+ filterText: filterText ,
126+ totalItemsInQuery: computed (() => totalRows .value ),
127+ allItems: gridData ,
128+ filterClass: filterClass ,
129+ selectable: computed (() => Boolean (props .gridConfig .batch )),
130+ onDelete : (item ) => {
131+ // TODO: Is this a naive way to find the delete operation?
132+ const fieldEntry = props .gridConfig .fields .find ((f ) => f .type === " operations" );
133+ const operation = fieldEntry ?.operations ?.find ((o ) => o .title === " Delete" );
134+ if (operation ) {
135+ operation .handler (item );
136+ }
137+ },
138+ expectedKeyDownClass: " grid-data-tbody" ,
139+ });
140+ const selectedIndeterminate = computed (() => ! [0 , gridData .value .length ].includes (selectionSize .value ));
141+
142+ function isRangeSelectAnchor(rowData : RowData ): boolean {
143+ if (initSelectedItem .value ) {
144+ const itemKey = props .gridConfig .getItemKey ?? getItemKey ;
145+ return itemKey (rowData ) === itemKey (initSelectedItem .value );
146+ }
147+ return false ;
148+ }
149+
150+ function getItemKey(item : RowData ): string {
151+ if (item && typeof item === " object" && " id" in item ) {
152+ return String (item .id );
153+ }
154+ return JSON .stringify (item );
155+ }
156+
112157/**
113158 * Manually set filter value, used for tags and `SharingIndicators`
114159 */
@@ -159,7 +204,7 @@ function fieldTitle(fieldEntry: FieldEntry): string | null {
159204 */
160205async function getGridData() {
161206 resultsLoading .value = true ;
162- selected . value = new Set ();
207+ resetSelection ();
163208 if (props .gridConfig ) {
164209 if (hasInvalidFilters .value ) {
165210 // there are invalid filters, so we don't want to search
@@ -250,24 +295,6 @@ function onFilter(filter?: string) {
250295 }
251296}
252297
253- // Select multiple rows
254- function onSelect(rowData : RowData ) {
255- if (selected .value .has (rowData )) {
256- selected .value .delete (rowData );
257- } else {
258- selected .value .add (rowData );
259- }
260- selected .value = new Set (selected .value );
261- }
262-
263- function onSelectAll(current : boolean ): void {
264- if (current ) {
265- selected .value = new Set (gridData .value );
266- } else {
267- selected .value = new Set ();
268- }
269- }
270-
271298/**
272299 * Show details for a row
273300 */
@@ -396,9 +423,9 @@ watch(operationMessage, () => {
396423 <th v-if =" !!gridConfig.batch" >
397424 <BFormCheckbox
398425 class =" m-2"
399- :checked =" selectedAll "
426+ :checked =" allSelected "
400427 :indeterminate =" selectedIndeterminate"
401- @change =" onSelectAll " />
428+ @change =" selectAllInCurrentQuery " />
402429 </th >
403430 <th
404431 v-for =" (fieldEntry, fieldIndex) in gridConfig.fields"
@@ -424,14 +451,25 @@ watch(operationMessage, () => {
424451 <span v-else-if =" fieldTitle(fieldEntry)" >{{ fieldTitle(fieldEntry) }}</span >
425452 </th >
426453 </thead >
427- <tbody v-for =" (rowData, rowIndex) in gridData" :key =" rowIndex" data-description =" grid item" >
454+ <tbody
455+ v-for =" (rowData, rowIndex) in gridData"
456+ :id =" gridConfig.getItemKey ? gridConfig.getItemKey(rowData) : getItemKey(rowData)"
457+ :ref =" itemRefs[gridConfig.getItemKey ? gridConfig.getItemKey(rowData) : getItemKey(rowData)]"
458+ :key =" rowIndex"
459+ class =" grid-data-tbody"
460+ :class =" { 'range-select-anchor-item': isRangeSelectAnchor(rowData) }"
461+ data-description =" grid item"
462+ tabindex =" 0"
463+ role =" row"
464+ @keydown =" onKeyDown(rowData, $event)"
465+ @click =" onClick(rowData, $event)" >
428466 <tr :class =" { 'grid-dark-row': rowIndex % 2 }" >
429467 <td v-if =" !!gridConfig.batch" >
430468 <BFormCheckbox
431- :checked =" selected.has (rowData)"
469+ :checked =" isSelected (rowData)"
432470 class =" m-2 cursor-pointer"
433471 data-description =" grid selected"
434- @change =" onSelect (rowData)" />
472+ @change =" setSelected (rowData, !isSelected(rowData) )" />
435473 </td >
436474 <td
437475 v-for =" (fieldEntry, fieldIndex) in gridConfig.fields"
@@ -517,16 +555,16 @@ watch(operationMessage, () => {
517555 <div v-for =" (batchOperation, batchIndex) in gridConfig.batch" :key =" batchIndex" >
518556 <GButton
519557 v-if ="
520- selected.size > 0 &&
521- (!batchOperation.condition || batchOperation.condition(Array.from(selected)))
558+ selectionSize > 0 &&
559+ (!batchOperation.condition || batchOperation.condition(Array.from(selected.values() )))
522560 "
523561 class =" mr-2"
524562 size =" small"
525563 color =" blue"
526564 :data-description =" `grid batch ${batchOperation.title.toLowerCase()}`"
527- @click =" onBatchOperation(batchOperation, Array.from(selected))" >
565+ @click =" onBatchOperation(batchOperation, Array.from(selected.values() ))" >
528566 <Icon :icon =" batchOperation.icon" class =" mr-1" />
529- <span v-localize >{{ batchOperation.title }} ({{ selected.size }})</span >
567+ <span v-localize >{{ batchOperation.title }} ({{ selectionSize }})</span >
530568 </GButton >
531569 </div >
532570 </div >
@@ -568,4 +606,10 @@ watch(operationMessage, () => {
568606.grid-dark-row {
569607 background : $gray-200 ;
570608}
609+
610+ .grid-data-tbody {
611+ & .range-select-anchor-item {
612+ box-shadow : 0 0 0 0.2rem transparentize ($brand-primary , 0.75 );
613+ }
614+ }
571615 </style >
0 commit comments