fix(grid): preserve selection across column reordering
This commit is contained in:
1 parent
b406c0a52c
commit
d63a407931
4 files changed
+189
-18
No files matched your search
@@ -1910,15 +1910,15 @@ const {
|
||||
invertColumnVisibility,
|
||||
showColumn,
|
||||
persistColumnOrder,
|
||||
moveDisplayableColumn,
|
||||
resetColumnOrder,
|
||||
moveDisplayableColumn: moveDisplayableColumnInLayout,
|
||||
resetColumnOrder: resetColumnOrderInLayout,
|
||||
toggleAllNullColumns,
|
||||
resetColumnVisibility,
|
||||
onTableDataGridColumnOrderChanged,
|
||||
frozenColumnCount,
|
||||
freezeToColumn,
|
||||
freezeSelectedColumns,
|
||||
unfreezeAllColumns,
|
||||
freezeSelectedColumns: freezeSelectedColumnsInLayout,
|
||||
unfreezeAllColumns: unfreezeAllColumnsInLayout,
|
||||
} = useDataGridColumnLayoutState({
|
||||
columns: computed(() => props.result.columns),
|
||||
sourceColumns: computed(() => props.sourceColumns),
|
||||
@@ -2136,13 +2136,36 @@ let gridScrollLeftBeforeTranspose = 0;
|
||||
let gridScrollTopBeforeKeyboardTranspose: number | null = null;
|
||||
let restoreGridScrollTopAfterTranspose = false;
|
||||
|
||||
function persistDraggedColumnOrder(indexes: number[]) {
|
||||
function applyColumnOrderChange(change: () => void) {
|
||||
const previousVisibleColumnIndexes = [...visibleColumnIndexes.value];
|
||||
persistColumnOrder(indexes);
|
||||
selection.remapColumnSelection(
|
||||
previousVisibleColumnIndexes,
|
||||
indexes.filter((index) => !hiddenColumnIndexes.value.has(index)),
|
||||
);
|
||||
change();
|
||||
const nextVisibleColumnIndexes = [...visibleColumnIndexes.value];
|
||||
if (previousVisibleColumnIndexes.length === nextVisibleColumnIndexes.length && previousVisibleColumnIndexes.every((index, position) => index === nextVisibleColumnIndexes[position])) return;
|
||||
selection.reconcileSelectionAfterColumnReorder(previousVisibleColumnIndexes, nextVisibleColumnIndexes);
|
||||
}
|
||||
|
||||
function onSynchronizedTableDataGridColumnOrderChanged(event: Event) {
|
||||
applyColumnOrderChange(() => onTableDataGridColumnOrderChanged(event));
|
||||
}
|
||||
|
||||
function persistDraggedColumnOrder(indexes: number[]) {
|
||||
applyColumnOrderChange(() => persistColumnOrder(indexes));
|
||||
}
|
||||
|
||||
function moveDisplayableColumn(fromDisplayableIndex: number, toDisplayableIndex: number) {
|
||||
applyColumnOrderChange(() => moveDisplayableColumnInLayout(fromDisplayableIndex, toDisplayableIndex));
|
||||
}
|
||||
|
||||
function resetColumnOrder() {
|
||||
applyColumnOrderChange(resetColumnOrderInLayout);
|
||||
}
|
||||
|
||||
function freezeSelectedColumns(selectedVisibleColumnIndexes: number[]) {
|
||||
applyColumnOrderChange(() => freezeSelectedColumnsInLayout(selectedVisibleColumnIndexes));
|
||||
}
|
||||
|
||||
function unfreezeAllColumns() {
|
||||
applyColumnOrderChange(unfreezeAllColumnsInLayout);
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -6304,7 +6327,7 @@ onMounted(() => {
|
||||
window.addEventListener("resize", refreshDataGridViewportMetrics);
|
||||
window.visualViewport?.addEventListener("resize", refreshDataGridViewportMetrics);
|
||||
window.addEventListener("dbx:ui-scale-applied", refreshDataGridViewportMetrics);
|
||||
window.addEventListener(TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, onTableDataGridColumnOrderChanged);
|
||||
window.addEventListener(TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, onSynchronizedTableDataGridColumnOrderChanged);
|
||||
window.addEventListener("blur", clearInternalClipboardCopy);
|
||||
document.addEventListener("visibilitychange", clearInternalClipboardCopy);
|
||||
});
|
||||
@@ -6326,7 +6349,7 @@ onUnmounted(() => {
|
||||
window.removeEventListener("resize", refreshDataGridViewportMetrics);
|
||||
window.visualViewport?.removeEventListener("resize", refreshDataGridViewportMetrics);
|
||||
window.removeEventListener("dbx:ui-scale-applied", refreshDataGridViewportMetrics);
|
||||
window.removeEventListener(TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, onTableDataGridColumnOrderChanged);
|
||||
window.removeEventListener(TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, onSynchronizedTableDataGridColumnOrderChanged);
|
||||
window.removeEventListener("blur", clearInternalClipboardCopy);
|
||||
document.removeEventListener("visibilitychange", clearInternalClipboardCopy);
|
||||
});
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("DataGrid synchronized column order selection", () => {
|
||||
it("reconciles the active selection when another grid changes the table order", () => {
|
||||
expect(dataGridSource).toContain("applyColumnOrderChange(() => onTableDataGridColumnOrderChanged(event));");
|
||||
expect(dataGridSource).toContain("window.addEventListener(TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, onSynchronizedTableDataGridColumnOrderChanged);");
|
||||
expect(dataGridSource).toContain("window.removeEventListener(TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, onSynchronizedTableDataGridColumnOrderChanged);");
|
||||
});
|
||||
});
|
||||
@@ -45,7 +45,7 @@ describe("useDataGridSelection", () => {
|
||||
|
||||
selection.selectColumn(0);
|
||||
selection.selectColumn(2, rowEvent({ meta: true }));
|
||||
selection.remapColumnSelection([0, 1, 2], [1, 2, 0]);
|
||||
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [1, 2, 0]);
|
||||
|
||||
expect(selection.selectedColumnIndexes.value).toEqual(new Set([1, 2]));
|
||||
|
||||
@@ -53,6 +53,92 @@ describe("useDataGridSelection", () => {
|
||||
expect(selection.selectedColumnIndexes.value).toEqual(new Set([0, 1, 2]));
|
||||
});
|
||||
|
||||
it("moves a single-column rectangular cell selection with its column after reordering", () => {
|
||||
const selection = createSelection();
|
||||
|
||||
selection.selectSingleCell(1, 0);
|
||||
selection.extendCellSelectionTo(2, 0);
|
||||
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [1, 2, 0]);
|
||||
|
||||
expect(selection.selectedRange.value).toEqual({ startRow: 1, endRow: 2, startCol: 2, endCol: 2 });
|
||||
expect(selection.cellIsSelected(1, 2)).toBe(true);
|
||||
expect(selection.cellIsSelected(2, 0)).toBe(false);
|
||||
});
|
||||
|
||||
it("moves a discrete single-column cell selection with its column after reordering", () => {
|
||||
const selection = createSelection();
|
||||
|
||||
selection.selectedCellKeys.value = new Set(["0:0", "2:0"]);
|
||||
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [1, 2, 0]);
|
||||
|
||||
expect(selection.selectedCellKeys.value).toEqual(new Set(["0:2", "2:2"]));
|
||||
expect(selection.cellIsSelected(0, 2)).toBe(true);
|
||||
expect(selection.cellIsSelected(2, 0)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps a multi-column rectangular cell selection when its columns remain contiguous", () => {
|
||||
const selection = createSelection();
|
||||
|
||||
selection.selectSingleCell(1, 0);
|
||||
selection.extendCellSelectionTo(2, 1);
|
||||
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [2, 0, 1]);
|
||||
|
||||
expect(selection.selectedRange.value).toEqual({ startRow: 1, endRow: 2, startCol: 1, endCol: 2 });
|
||||
expect(selection.cellIsSelected(1, 1)).toBe(true);
|
||||
expect(selection.cellIsSelected(2, 2)).toBe(true);
|
||||
expect(selection.cellIsSelected(1, 0)).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ edge: "left", nextColumnIndexes: [1, 0, 2] },
|
||||
{ edge: "right", nextColumnIndexes: [0, 2, 1] },
|
||||
])("keeps the whole rectangular selection when an inner column moves to its $edge edge", ({ nextColumnIndexes }) => {
|
||||
const selection = createSelection();
|
||||
|
||||
selection.selectSingleCell(1, 0);
|
||||
selection.extendCellSelectionTo(2, 2);
|
||||
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], nextColumnIndexes);
|
||||
|
||||
expect(selection.selectedRange.value).toEqual({ startRow: 1, endRow: 2, startCol: 0, endCol: 2 });
|
||||
expect(selection.cellIsSelected(1, nextColumnIndexes.indexOf(1))).toBe(true);
|
||||
expect(selection.selectedCellCount.value).toBe(6);
|
||||
});
|
||||
|
||||
it("clears a multi-column rectangular cell selection when its columns split apart", () => {
|
||||
const selection = createSelection();
|
||||
|
||||
selection.selectSingleCell(1, 0);
|
||||
selection.extendCellSelectionTo(2, 1);
|
||||
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [1, 2, 0]);
|
||||
|
||||
expect(selection.selectedRange.value).toBeNull();
|
||||
expect(selection.selectedCellKeys.value).toEqual(new Set());
|
||||
expect(selection.hasCellSelection.value).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps a discrete multi-column cell selection when its columns become contiguous", () => {
|
||||
const selection = createSelection();
|
||||
|
||||
selection.selectedCellKeys.value = new Set(["0:0", "0:2", "2:0", "2:2"]);
|
||||
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [1, 2, 0]);
|
||||
|
||||
expect(selection.selectedCellKeys.value).toEqual(new Set(["0:2", "0:1", "2:2", "2:1"]));
|
||||
expect(selection.cellIsSelected(0, 1)).toBe(true);
|
||||
expect(selection.cellIsSelected(2, 2)).toBe(true);
|
||||
expect(selection.cellIsSelected(0, 0)).toBe(false);
|
||||
});
|
||||
|
||||
it("clears a discrete multi-column cell selection when its columns split apart", () => {
|
||||
const selection = createSelection();
|
||||
|
||||
selection.selectedCellKeys.value = new Set(["0:0", "0:1", "2:0", "2:1"]);
|
||||
selection.reconcileSelectionAfterColumnReorder([0, 1, 2], [1, 2, 0]);
|
||||
|
||||
expect(selection.selectedRange.value).toBeNull();
|
||||
expect(selection.selectedCellKeys.value).toEqual(new Set());
|
||||
expect(selection.hasCellSelection.value).toBe(false);
|
||||
});
|
||||
|
||||
it("invalidates synthetic context state for ordinary, Ctrl, and Cmd cell selection", () => {
|
||||
const originalDocument = globalThis.document;
|
||||
const fakeDocument = {
|
||||
|
||||
@@ -247,13 +247,63 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
lastClickedColumnIndex.value = colIndex;
|
||||
}
|
||||
|
||||
function remapColumnSelection(previousColumnIndexes: readonly number[], nextColumnIndexes: readonly number[]) {
|
||||
const selectedColumns = new Set([...selectedColumnIndexes.value].map((index) => previousColumnIndexes[index]).filter((index): index is number => index !== undefined));
|
||||
selectedColumnIndexes.value = new Set(nextColumnIndexes.flatMap((columnIndex, index) => (selectedColumns.has(columnIndex) ? [index] : [])));
|
||||
|
||||
function reconcileSelectionAfterColumnReorder(previousColumnIndexes: readonly number[], nextColumnIndexes: readonly number[]) {
|
||||
const lastClickedColumn = lastClickedColumnIndex.value === null ? undefined : previousColumnIndexes[lastClickedColumnIndex.value];
|
||||
const nextLastClickedColumnIndex = lastClickedColumn === undefined ? -1 : nextColumnIndexes.indexOf(lastClickedColumn);
|
||||
lastClickedColumnIndex.value = nextLastClickedColumnIndex >= 0 ? nextLastClickedColumnIndex : null;
|
||||
|
||||
if (hasColumnSelection.value) {
|
||||
const selectedColumns = new Set([...selectedColumnIndexes.value].map((index) => previousColumnIndexes[index]).filter((index): index is number => index !== undefined));
|
||||
selectedColumnIndexes.value = new Set(nextColumnIndexes.flatMap((columnIndex, index) => (selectedColumns.has(columnIndex) ? [index] : [])));
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedCellColumnIndexes = cellSelectionColumnIndexes();
|
||||
const remappedCellColumnIndexes = selectedCellColumnIndexes.map((index) => {
|
||||
const selectedColumn = previousColumnIndexes[index];
|
||||
return selectedColumn === undefined ? -1 : nextColumnIndexes.indexOf(selectedColumn);
|
||||
});
|
||||
const sortedRemappedCellColumnIndexes = [...remappedCellColumnIndexes].sort((a, b) => a - b);
|
||||
const selectionRemainsContiguous = sortedRemappedCellColumnIndexes.length > 0 && sortedRemappedCellColumnIndexes.every((index, position) => index >= 0 && index === sortedRemappedCellColumnIndexes[0]! + position);
|
||||
if (selectionRemainsContiguous) {
|
||||
const remappedColumnIndexByPreviousIndex = new Map(selectedCellColumnIndexes.map((index, position) => [index, remappedCellColumnIndexes[position]!]));
|
||||
if (selectedCellKeys.value.size > 0) {
|
||||
selectedCellKeys.value = new Set(
|
||||
[...selectedCellKeys.value].map((key) => {
|
||||
const position = parseCellKey(key)!;
|
||||
return cellKey(position.rowIndex, remappedColumnIndexByPreviousIndex.get(position.colIndex)!);
|
||||
}),
|
||||
);
|
||||
if (selectionAnchor.value) selectionAnchor.value = { ...selectionAnchor.value, colIndex: remappedColumnIndexByPreviousIndex.get(selectionAnchor.value.colIndex)! };
|
||||
if (selectionFocus.value) selectionFocus.value = { ...selectionFocus.value, colIndex: remappedColumnIndexByPreviousIndex.get(selectionFocus.value.colIndex)! };
|
||||
} else if (selectionAnchor.value && selectionFocus.value) {
|
||||
const selectionStartsLeft = selectionAnchor.value.colIndex <= selectionFocus.value.colIndex;
|
||||
const startCol = sortedRemappedCellColumnIndexes[0]!;
|
||||
const endCol = sortedRemappedCellColumnIndexes[sortedRemappedCellColumnIndexes.length - 1]!;
|
||||
selectionAnchor.value = { ...selectionAnchor.value, colIndex: selectionStartsLeft ? startCol : endCol };
|
||||
selectionFocus.value = { ...selectionFocus.value, colIndex: selectionStartsLeft ? endCol : startCol };
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
clearCellSelection();
|
||||
lastClickedColumnIndex.value = null;
|
||||
}
|
||||
|
||||
function cellSelectionColumnIndexes(): number[] {
|
||||
if (selectedCellKeys.value.size > 0) {
|
||||
const selectedColumnIndexes = new Set<number>();
|
||||
for (const key of selectedCellKeys.value) {
|
||||
const position = parseCellKey(key);
|
||||
if (!position) return [];
|
||||
selectedColumnIndexes.add(position.colIndex);
|
||||
}
|
||||
return [...selectedColumnIndexes];
|
||||
}
|
||||
|
||||
const range = selectedRange.value;
|
||||
if (!range) return [];
|
||||
return Array.from({ length: range.endCol - range.startCol + 1 }, (_, index) => range.startCol + index);
|
||||
}
|
||||
|
||||
function selectAllCells() {
|
||||
@@ -588,7 +638,7 @@ export function useDataGridSelection(options: UseDataGridSelectionOptions) {
|
||||
selectRow,
|
||||
selectColumn,
|
||||
selectColumns,
|
||||
remapColumnSelection,
|
||||
reconcileSelectionAfterColumnReorder,
|
||||
selectAllCells,
|
||||
extendCellSelectionTo,
|
||||
finishCellSelection,
|
||||
|
||||
Reference in new issue
Block a user