fix(grid): size columns by sampled values and distribute spare width
This commit is contained in:
3 files changed
+97
-13
No files matched your search
@@ -2282,6 +2282,7 @@ function measureColumnHeaderText(text: string): number | undefined {
|
||||
}
|
||||
|
||||
let columnFormatterForWidth: ((columnIndex: number) => ColumnFormatterConfig | undefined) | undefined;
|
||||
const gridViewportWidth = ref(0);
|
||||
|
||||
function columnWidthDisplayValue(value: CellValue, columnIndex: number): CellValue {
|
||||
const formatter = columnFormatterForWidth?.(columnIndex);
|
||||
@@ -2301,6 +2302,7 @@ const { initColumnWidths, onResizeStart, autoFitColumn, autoFitAllColumns, rende
|
||||
measureHeaderText: measureColumnHeaderText,
|
||||
headerMeasurementKey: columnHeaderMeasurementKey,
|
||||
rowNumberWidth,
|
||||
viewportWidth: gridViewportWidth,
|
||||
displayValue: columnWidthDisplayValue,
|
||||
});
|
||||
const gridStyle = computed(() => ({
|
||||
@@ -2312,7 +2314,6 @@ const gridStyle = computed(() => ({
|
||||
"--dbx-table-font-size": `${tableFontSize.value}px`,
|
||||
}));
|
||||
const gridHorizontalScrollLeft = ref(0);
|
||||
const gridViewportWidth = ref(0);
|
||||
let gridScrollLeftBeforeTranspose = 0;
|
||||
let gridScrollTopBeforeKeyboardTranspose: number | null = null;
|
||||
let restoreGridScrollTopAfterTranspose = false;
|
||||
|
||||
@@ -17,6 +17,7 @@ function createResizeState(options: {
|
||||
compactColumnHeaderActions?: boolean;
|
||||
indexIndicatorColumnIndexes?: number[] | ReturnType<typeof ref<number[]>>;
|
||||
headerTextWidth?: number;
|
||||
viewportWidth?: number | ReturnType<typeof ref<number>>;
|
||||
displayValue?: (value: string | number | boolean | null, columnIndex: number) => string | number | boolean | null;
|
||||
}) {
|
||||
const compact = ref(options.compactColumnHeaderActions ?? true);
|
||||
@@ -25,6 +26,7 @@ function createResizeState(options: {
|
||||
const density = ref(options.density ?? "standard");
|
||||
const rows = isRef(options.rows) ? options.rows : ref(options.rows);
|
||||
const indexIndicatorColumnIndexes = isRef(options.indexIndicatorColumnIndexes) ? options.indexIndicatorColumnIndexes : ref(options.indexIndicatorColumnIndexes ?? []);
|
||||
const viewportWidth = isRef(options.viewportWidth) ? options.viewportWidth : ref(options.viewportWidth ?? 0);
|
||||
const state = useDataGridColumnResize({
|
||||
columns: computed(() => options.columns),
|
||||
sourceRows: computed(() => rows.value),
|
||||
@@ -36,6 +38,7 @@ function createResizeState(options: {
|
||||
columnStructureSignature: computed(() => createDataGridColumnStructureSignature(options.columns, options.columnTypes)),
|
||||
measureHeaderText: () => headerTextWidth.value,
|
||||
headerMeasurementKey,
|
||||
viewportWidth,
|
||||
displayValue: options.displayValue,
|
||||
});
|
||||
return {
|
||||
@@ -87,6 +90,52 @@ describe("useDataGridColumnResize", () => {
|
||||
expect(after.length).toBe(before.length);
|
||||
});
|
||||
|
||||
it("distributes spare viewport width across non-user-sized columns", () => {
|
||||
const state = createResizeState({
|
||||
columns: ["id", "name"],
|
||||
rows: [[1, "a"]],
|
||||
viewportWidth: 400,
|
||||
});
|
||||
state.initColumnWidths();
|
||||
const intrinsic = state.columnWidths.value.slice();
|
||||
expect(state.renderedColumnWidths.value.reduce((sum, width) => sum + width, 0) + DATA_GRID_ROW_NUM_WIDTH).toBe(400);
|
||||
expect(state.renderedColumnWidths.value[0]).toBeGreaterThan(intrinsic[0]);
|
||||
expect(state.renderedColumnWidths.value[1]).toBeGreaterThan(intrinsic[1]);
|
||||
|
||||
state.onResizeStart(0, new MouseEvent("mousedown", { clientX: 100, cancelable: true }));
|
||||
document.dispatchEvent(new MouseEvent("mouseup", { clientX: 100 }));
|
||||
expect(state.renderedColumnWidths.value[0]).toBeGreaterThan(intrinsic[0]);
|
||||
expect(state.renderedColumnWidths.value[1]).toBeGreaterThan(intrinsic[1]);
|
||||
});
|
||||
|
||||
it("tracks the cursor while dragging under spare width and stores the final width without stale rendering", async () => {
|
||||
const state = createResizeState({
|
||||
columns: ["id", "name"],
|
||||
rows: [[1, "a"]],
|
||||
viewportWidth: 1000,
|
||||
});
|
||||
state.initColumnWidths();
|
||||
const intrinsic = state.columnWidths.value.slice();
|
||||
const startWidth = state.renderedColumnWidths.value[0] ?? 0;
|
||||
// The grabbed column initially renders wider than its stored intrinsic width.
|
||||
expect(startWidth).toBeGreaterThan(intrinsic[0] ?? 0);
|
||||
|
||||
state.onResizeStart(0, new MouseEvent("mousedown", { clientX: 100, cancelable: true }));
|
||||
// Grabbing the handle must not shift the rendered edge.
|
||||
expect(state.renderedColumnWidths.value[0]).toBe(startWidth);
|
||||
|
||||
document.dispatchEvent(new MouseEvent("mousemove", { clientX: 150 }));
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
// Mid-drag the rendered width is exactly startWidth + delta: the resizing
|
||||
// column is not re-stretched with the remaining surplus.
|
||||
expect(state.renderedColumnWidths.value[0]).toBe(startWidth + 50);
|
||||
|
||||
document.dispatchEvent(new MouseEvent("mouseup", { clientX: 150 }));
|
||||
// After mouseup the rendered width equals the stored user width (no stale render).
|
||||
expect(state.columnWidths.value[0]).toBe(startWidth + 50);
|
||||
expect(state.renderedColumnWidths.value[0]).toBe(state.columnWidths.value[0]);
|
||||
});
|
||||
|
||||
it("keeps auto fitted widths when the grid re-initialises for the same columns", () => {
|
||||
const longText = "y".repeat(220);
|
||||
const state = createResizeState({
|
||||
@@ -112,13 +161,17 @@ describe("useDataGridColumnResize", () => {
|
||||
expect(state.renderedColumnWidths.value).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps compact query result columns at content width instead of filling the viewport", () => {
|
||||
it("keeps compact query result columns at content width when no viewport width is reported", () => {
|
||||
// viewportWidth unset (0) means no spare width is distributed, so compact
|
||||
// columns keep their content width; with a real viewport they fill it — see
|
||||
// "distributes spare viewport width across non-user-sized columns" above.
|
||||
const state = createResizeState({
|
||||
columns: ["id", "user_id"],
|
||||
rows: [
|
||||
[1, 10],
|
||||
[2, 20],
|
||||
],
|
||||
viewportWidth: 0,
|
||||
});
|
||||
|
||||
state.initColumnWidths();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ref, computed, watch, type ComputedRef, type Ref } from "vue";
|
||||
import { ref, shallowRef, computed, watch, type ComputedRef, type Ref } from "vue";
|
||||
import { calculateDataGridColumnWidth, DATA_GRID_AUTO_FIT_VALUE_TEXT_LIMIT, DATA_GRID_COL_AUTO_FIT_MAX_WIDTH, DATA_GRID_COL_MIN_WIDTH, COLUMN_WIDTH_DENSITY_PRESETS, sampleDataGridColumnValues } from "@/lib/dataGrid/dataGridColumnWidth";
|
||||
import { createDataGridColumnMeasurementSignature, loadDataGridColumnWidthState, removeDataGridColumnWidthState, saveDataGridColumnWidthState } from "@/lib/dataGrid/dataGridColumnWidthState";
|
||||
import type { ColumnWidthDensity } from "@/stores/settingsStore";
|
||||
@@ -42,6 +42,7 @@ export interface UseDataGridColumnResizeOptions {
|
||||
measureHeaderText?: (text: string) => number | undefined;
|
||||
headerMeasurementKey?: Ref<unknown>;
|
||||
rowNumberWidth?: Ref<number> | ComputedRef<number>;
|
||||
viewportWidth?: Ref<number> | ComputedRef<number>;
|
||||
displayValue?: (value: CellValue, columnIndex: number) => CellValue;
|
||||
}
|
||||
|
||||
@@ -51,7 +52,9 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
|
||||
const columnWidths = ref<number[]>([]);
|
||||
let isResizing = false;
|
||||
let previousColumnIndexes: number[] = [];
|
||||
let userSizedColumnIndexes = new Set<number>();
|
||||
// Replaced wholesale instead of mutated so reactive consumers (renderedColumnWidths)
|
||||
// re-run when a column becomes user-sized and stops receiving spare width.
|
||||
const userSizedColumnIndexes = shallowRef<Set<number>>(new Set());
|
||||
|
||||
function columnWidthStateIdentity() {
|
||||
return {
|
||||
@@ -62,7 +65,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
|
||||
}
|
||||
|
||||
function persistColumnWidths() {
|
||||
saveDataGridColumnWidthState(columnWidthStateIdentity(), previousColumnIndexes, columnWidths.value, userSizedColumnIndexes);
|
||||
saveDataGridColumnWidthState(columnWidthStateIdentity(), previousColumnIndexes, columnWidths.value, userSizedColumnIndexes.value);
|
||||
}
|
||||
|
||||
function sampleColumnValues(visibleColIdx: number): CellValue[] {
|
||||
@@ -96,7 +99,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
|
||||
const next = columnWidths.value.slice();
|
||||
for (let colIdx = 0; colIdx < columns.value.length; colIdx++) {
|
||||
const actualColIdx = columnIndexes.value[colIdx];
|
||||
if (actualColIdx === undefined || userSizedColumnIndexes.has(actualColIdx)) continue;
|
||||
if (actualColIdx === undefined || userSizedColumnIndexes.value.has(actualColIdx)) continue;
|
||||
const needed = neededWidths[colIdx] ?? DATA_GRID_COL_MIN_WIDTH;
|
||||
if (needed > (next[colIdx] ?? 0)) {
|
||||
next[colIdx] = needed;
|
||||
@@ -110,11 +113,14 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
|
||||
|
||||
function markColumnUserSized(visibleColIdx: number) {
|
||||
const actualColIdx = columnIndexes.value[visibleColIdx];
|
||||
if (actualColIdx !== undefined) userSizedColumnIndexes.add(actualColIdx);
|
||||
if (actualColIdx === undefined || userSizedColumnIndexes.value.has(actualColIdx)) return;
|
||||
const next = new Set(userSizedColumnIndexes.value);
|
||||
next.add(actualColIdx);
|
||||
userSizedColumnIndexes.value = next;
|
||||
}
|
||||
|
||||
function initColumnWidths(force = false) {
|
||||
if (force) userSizedColumnIndexes.clear();
|
||||
if (force) userSizedColumnIndexes.value = new Set();
|
||||
const previousWidthsByColumnIndex = new Map<number, number>();
|
||||
previousColumnIndexes.forEach((columnIndex, visibleIndex) => {
|
||||
const width = columnWidths.value[visibleIndex];
|
||||
@@ -122,7 +128,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
|
||||
});
|
||||
const nextColumnIndexes = [...columnIndexes.value];
|
||||
const cachedState = !force && previousColumnIndexes.length === 0 ? loadDataGridColumnWidthState(columnWidthStateIdentity(), nextColumnIndexes) : undefined;
|
||||
if (cachedState) userSizedColumnIndexes = new Set(cachedState.userSizedColumnIndexes);
|
||||
if (cachedState) userSizedColumnIndexes.value = new Set(cachedState.userSizedColumnIndexes);
|
||||
const currentNeededWidths = neededColumnWidths.value ?? [];
|
||||
const columnCount = columns.value?.length ?? 0;
|
||||
if (force || columnWidths.value.length !== columnCount || previousColumnIndexes.join("\0") !== nextColumnIndexes.join("\0")) {
|
||||
@@ -144,7 +150,15 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
|
||||
event.preventDefault();
|
||||
isResizing = true;
|
||||
const startX = event.clientX;
|
||||
const startWidth = columnWidths.value[colIdx] ?? DATA_GRID_COL_MIN_WIDTH;
|
||||
// The drag base is the width the user grabbed, which may include spare width
|
||||
// distributed at render time. Freezing that snapshot as the stored width and
|
||||
// marking the column user-sized right away keeps the column out of the
|
||||
// flexible set for the whole drag, so every frame renders startWidth + delta
|
||||
// as-is instead of re-stretching it with the remaining surplus, and the
|
||||
// stored (persisted) width matches what the cursor moved.
|
||||
const startWidth = renderedColumnWidths.value[colIdx] ?? DATA_GRID_COL_MIN_WIDTH;
|
||||
markColumnUserSized(colIdx);
|
||||
columnWidths.value[colIdx] = startWidth;
|
||||
let pendingClientX = startX;
|
||||
let resizeFrame = 0;
|
||||
|
||||
@@ -232,10 +246,26 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
|
||||
persistColumnWidths();
|
||||
}
|
||||
|
||||
const renderedColumnWidths = computed(() => columnWidths.value.slice());
|
||||
|
||||
const resolvedRowNumberWidth = computed(() => options.rowNumberWidth?.value ?? DATA_GRID_ROW_NUM_WIDTH);
|
||||
|
||||
const renderedColumnWidths = computed(() => {
|
||||
const widths = columnWidths.value.slice();
|
||||
const viewportWidth = options.viewportWidth?.value ?? 0;
|
||||
const surplus = viewportWidth - resolvedRowNumberWidth.value - widths.reduce((sum, width) => sum + width, 0);
|
||||
if (surplus <= 0) return widths;
|
||||
const flexibleIndexes = widths.flatMap((_, visibleIndex) => {
|
||||
const actualIndex = columnIndexes.value[visibleIndex];
|
||||
return actualIndex !== undefined && !userSizedColumnIndexes.value.has(actualIndex) ? [visibleIndex] : [];
|
||||
});
|
||||
if (flexibleIndexes.length === 0) return widths;
|
||||
const base = Math.floor(surplus / flexibleIndexes.length);
|
||||
let remainder = surplus - base * flexibleIndexes.length;
|
||||
flexibleIndexes.forEach((index) => {
|
||||
widths[index] += base + (remainder-- > 0 ? 1 : 0);
|
||||
});
|
||||
return widths;
|
||||
});
|
||||
|
||||
const totalWidth = computed(() => renderedColumnWidths.value.reduce((a, b) => a + b, 0) + resolvedRowNumberWidth.value);
|
||||
|
||||
const columnVars = computed(() => {
|
||||
@@ -259,7 +289,7 @@ export function useDataGridColumnResize(options: UseDataGridColumnResizeOptions)
|
||||
watch([() => options.cacheKey?.value, options.columnStructureSignature], () => {
|
||||
columnWidths.value = [];
|
||||
previousColumnIndexes = [];
|
||||
userSizedColumnIndexes.clear();
|
||||
userSizedColumnIndexes.value = new Set();
|
||||
initColumnWidths();
|
||||
});
|
||||
watch([density, compactColumnHeaderActions, () => options.headerMeasurementKey?.value], () => {
|
||||
|
||||
Reference in new issue
Block a user