Forward Ref and selection/scrollto

This commit is contained in:
Hein 2025-10-24 16:51:55 +02:00
parent ad5bc14d7c
commit d6b7fa4076
8 changed files with 294 additions and 89 deletions

View File

@ -1,12 +1,14 @@
import '@glideapps/glide-data-grid/dist/index.css';
import React, { type Ref } from 'react';
import { MantineBetterMenusProvider } from '../MantineBetterMenu';
import { GlidlerFormAdaptor } from './components/adaptors/GlidlerFormAdaptor';
import { GlidlerLocalDataAdaptor } from './components/adaptors/GlidlerLocalDataAdaptor';
import { type GridlerProps, Provider } from './components/GridlerStore';
import { type GridlerProps, type GridlerRef, Provider } from './components/GridlerStore';
import { GridlerRefHandler } from './components/RefHandler';
import { GridlerDataGrid } from './GridlerDataGrid';
const Gridler = (props: GridlerProps) => {
const _Gridler = (props: GridlerProps, ref: Ref<GridlerRef> | undefined) => {
return (
<MantineBetterMenusProvider>
<Provider
@ -19,12 +21,20 @@ const Gridler = (props: GridlerProps) => {
}}
>
<GridlerDataGrid />
<GridlerRefHandler ref={ref} />
{props.children}
</Provider>
</MantineBetterMenusProvider>
);
};
type GridlerComponentType = {
FormAdaptor: typeof GlidlerFormAdaptor;
LocalDataAdaptor: typeof GlidlerLocalDataAdaptor;
} & React.ForwardRefExoticComponent<GridlerProps & React.RefAttributes<GridlerRef>>;
const Gridler = React.forwardRef(_Gridler) as GridlerComponentType;
Gridler.FormAdaptor = GlidlerFormAdaptor;
Gridler.LocalDataAdaptor = GlidlerLocalDataAdaptor;

View File

@ -40,6 +40,7 @@ export const GridlerDataGrid = () => {
const {
_gridSelection,
allowMultiSelect,
focused,
getCellContent,
getCellsForSelection,
@ -69,6 +70,7 @@ export const GridlerDataGrid = () => {
widthProp,
} = useGridlerStore((s) => ({
_gridSelection: s._gridSelection,
allowMultiSelect: s.allowMultiSelect,
focused: s.focused,
getCellContent: s.getCellContent,
getCellsForSelection: s.getCellsForSelection,
@ -157,16 +159,16 @@ export const GridlerDataGrid = () => {
height={height ?? 400}
overscrollX={16}
overscrollY={32}
rangeSelect="multi-rect"
rangeSelect={allowMultiSelect ? 'multi-rect' : 'cell'}
rightElementProps={{
fill: false,
sticky: true,
}}
rowMarkers={{
checkboxStyle: 'square',
kind: 'both',
kind: allowMultiSelect ? 'both' : 'clickable-number',
}}
rowSelect="multi"
rowSelect={allowMultiSelect ? 'multi' : 'single'}
rowSelectionMode="auto"
spanRangeBehavior="default"
{...glideProps}

View File

@ -10,32 +10,36 @@ export const Computer = React.memo(() => {
const {
_glideref,
_gridSelectionRows,
askAPIRowNumber,
colFilters,
colOrder,
colSize,
colSort,
columns,
getRowIndexByKey,
getState,
loadPage,
ready,
scrollToRowKey,
selectedRowKey,
setState,
setStateFN,
values,
} = useGridlerStore((s) => ({
_glideref: s._glideref,
_gridSelectionRows: s._gridSelectionRows,
askAPIRowNumber: s.askAPIRowNumber,
colFilters: s.colFilters,
colOrder: s.colOrder,
colSize: s.colSize,
colSort: s.colSort,
columns: s.columns,
getRowIndexByKey: s.getRowIndexByKey,
getState: s.getState,
loadPage: s.loadPage,
ready: s.ready,
scrollToRowKey: s.scrollToRowKey,
selectedRowKey: s.selectedRowKey,
setState: s.setState,
setStateFN: s.setStateFN,
uniqueid: s.uniqueid,
@ -72,8 +76,8 @@ export const Computer = React.memo(() => {
break;
}
}
if (!(rowIndex >= 0) && typeof askAPIRowNumber === 'function') {
const idx = await askAPIRowNumber(key);
if (!(rowIndex >= 0)) {
const idx = await getRowIndexByKey(key);
if (idx) {
rowIndexes.push(idx);
}
@ -180,12 +184,14 @@ export const Computer = React.memo(() => {
: (c.defaultIcon ?? 'sort'),
}));
}).then(() => {
loadPage(0, 'all');
getState('_events')?.dispatchEvent?.(
new CustomEvent('onColumnSorted', {
detail: { cols: colSort },
})
);
loadPage(0, 'all').then(() => {
getState('refreshCells')?.();
getState('_events')?.dispatchEvent?.(
new CustomEvent('onColumnSorted', {
detail: { cols: colSort },
})
);
});
});
}, [colSort]);
@ -195,13 +201,15 @@ export const Computer = React.memo(() => {
}
if (JSON.stringify(refLastFilters.current) !== JSON.stringify(colFilters)) {
loadPage(0, 'all');
loadPage(0, 'all').then(() => {
getState('refreshCells')?.();
getState('_events')?.dispatchEvent?.(
new CustomEvent('onColumnFiltered', {
detail: { filters: colFilters },
})
);
});
refLastFilters.current = colFilters;
getState('_events')?.dispatchEvent?.(
new CustomEvent('onColumnFiltered', {
detail: { filters: colFilters },
})
);
}
}, [colFilters]);
@ -214,6 +222,8 @@ export const Computer = React.memo(() => {
...c,
width: c.id && colSize?.[c.id] ? colSize?.[c.id] : c.width,
}));
}).then(() => {
getState('refreshCells')?.();
});
}, [colSize]);
@ -231,6 +241,8 @@ export const Computer = React.memo(() => {
});
return result;
}).then(() => {
getState('refreshCells')?.();
});
}, [colOrder]);
@ -242,7 +254,9 @@ export const Computer = React.memo(() => {
return;
}
refFirstRun.current = 1;
loadPage(0);
loadPage(0).then(() => {
getState('refreshCells')?.();
});
}, [ready, loadPage]);
useEffect(() => {
@ -250,30 +264,29 @@ export const Computer = React.memo(() => {
const loadPage = () => {
const selectFirstRowOnMount = getState('selectFirstRowOnMount');
if (selectFirstRowOnMount) {
const selectedRow = getState('selectedRow');
if (selectedRow && selectedRow >= 0) {
const scrollToRowKey = getState('scrollToRowKey');
if (scrollToRowKey && scrollToRowKey >= 0) {
return;
}
const keyField = getState('keyField') ?? 'id';
const page_data = getState('_page_data');
const firstBuffer = page_data?.[0]?.[0];
const firstRow = firstBuffer?.[keyField];
const currentValues = getState('values') ?? [];
if (firstRow && firstRow > 0) {
const values = [
firstBuffer,
...((getState('values') ?? []) as Array<Record<string, unknown>>),
];
if (firstRow && firstRow > 0 && (currentValues.length ?? 0) === 0) {
const values = [firstBuffer, ...(currentValues as Array<Record<string, unknown>>)];
const onChange = getState('onChange');
console.log('Selecting first row:', firstRow, firstBuffer, values);
//console.log('Selecting first row:', firstRow, firstBuffer, values);
if (onChange) {
onChange(values);
} else {
setState('values', values);
}
setState('selectedRow', firstRow);
setState('scrollToRowKey', firstRow);
}
}
};
@ -284,6 +297,64 @@ export const Computer = React.memo(() => {
_events?.removeEventListener('loadPage', loadPage);
};
}, []);
/// logic to apply the selected row.
useEffect(() => {
const ready = getState('ready');
const ref = getState('_glideref');
const getRowIndexByKey = getState('getRowIndexByKey');
if (scrollToRowKey && ref && ready) {
getRowIndexByKey?.(scrollToRowKey).then((r) => {
if (r !== undefined) {
console.log('Scrolling to selected row:', scrollToRowKey, r);
ref.scrollTo(0, r);
getState('_events').dispatchEvent(
new CustomEvent('scrollToRowKeyFound', {
detail: { rowNumber: r, scrollToRowKey: scrollToRowKey },
})
);
}
});
}
}, [scrollToRowKey]);
useEffect(() => {
const ready = getState('ready');
const ref = getState('_glideref');
const getRowIndexByKey = getState('getRowIndexByKey');
const key = selectedRowKey ?? scrollToRowKey;
if (key && ref && ready) {
getRowIndexByKey?.(key).then((r) => {
if (r !== undefined) {
console.log('Scrolling to selected row:', r, selectedRowKey, scrollToRowKey);
if (selectedRowKey) {
const onChange = getState('onChange');
const selected = [{ [getState('keyField') ?? 'id']: selectedRowKey }];
if (onChange) {
onChange(selected);
} else {
setState('values', selected);
}
}
ref.scrollTo(0, r);
getState('_events').dispatchEvent(
new CustomEvent('scrollToRowKeyFound', {
detail: {
rowNumber: r,
scrollToRowKey: scrollToRowKey,
selectedRowKey: selectedRowKey,
},
})
);
}
});
}
}, [scrollToRowKey, selectedRowKey]);
// console.log('Gridler:Debug:Computer', {
// colFilters,
// colOrder,

View File

@ -56,6 +56,7 @@ export type FilterOptionOperator =
| 'startswith';
export interface GridlerProps extends PropsWithChildren {
allowMultiSelect?: boolean;
columns?: GridlerColumns;
defaultSort?: Array<SortOption>;
@ -88,6 +89,7 @@ export interface GridlerProps extends PropsWithChildren {
) => GridCell;
rowHeight?: number;
scrollToRowKey?: number;
sections?: {
bottom?: React.ReactNode;
left?: React.ReactNode;
@ -97,7 +99,7 @@ export interface GridlerProps extends PropsWithChildren {
rightElementStart?: React.ReactNode;
top?: React.ReactNode;
};
selectedRow?: number;
selectedRowKey?: number;
selectFirstRowOnMount?: boolean;
selectMode?: 'cell' | 'row';
showMenu?: (id: string, options?: Partial<MantineBetterMenuInstance>) => void;
@ -110,6 +112,17 @@ export interface GridlerProps extends PropsWithChildren {
width?: number | string;
}
export interface GridlerRef {
getGlideRef: () => DataEditorRef | undefined;
getState: GridlerState['getState'];
refresh: (parms?: any) => Promise<void>;
reload: (parms?: any) => Promise<void>;
reloadRow: (key: number | string) => Promise<void>;
scrollToRow: (key: number | string) => Promise<void>;
selectRow: (key: number | string) => Promise<void>;
setStateFN: GridlerState['setStateFN'];
}
export interface GridlerState {
_active_requests?: Array<{ controller: AbortController; page: number }>;
_activeTooltip?: ReactNode;
@ -140,6 +153,7 @@ export interface GridlerState {
abortSignal: AbortSignal
) => CellArray | GetCellsThunk;
getRowBuffer: (row: number) => Record<string, any>;
getRowIndexByKey: (key: number | string) => Promise<number | undefined>;
getState: <K extends keyof GridlerStoreState>(key: K) => GridlerStoreState[K];
hasLocalData: boolean;
@ -174,7 +188,9 @@ export interface GridlerState {
pageSize: number;
ready: boolean;
refreshCells: (fromRow?: number, toRow?: number, col?: number) => void;
reload?: () => Promise<void>;
renderColumns?: GridlerColumns;
setState: <K extends keyof GridlerStoreState>(
key: K,
@ -250,6 +266,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
return result as CellArray;
};
},
getRowBuffer: (row: number) => {
const state = get();
//Handle local data
@ -272,6 +289,43 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
return rowData;
},
getRowIndexByKey: async (key: number | string) => {
const state = get();
let rowIndex = -1;
if (state.ready) {
const page_data = state._page_data;
const pageSize = state.pageSize;
const keyField = state.keyField ?? 'id';
for (const p in page_data) {
for (const r in page_data[p]) {
const idx = Number(p) * pageSize + Number(r);
//console.log('Found row', idx, page_data[p][r]?.[keyField], scrollToRowKey);
if (String(page_data[p][r]?.[keyField]) === String(key)) {
rowIndex =
page_data[p][r]?._rownumber > 0 ? page_data[p][r]?._rownumber : idx > 0 ? idx : -1;
break;
}
}
if (rowIndex > 0) {
console.log('Local row index', rowIndex, key);
return rowIndex;
}
}
if (rowIndex > 0) {
return rowIndex;
} else if (typeof state.askAPIRowNumber === 'function') {
const rn = await state.askAPIRowNumber(String(key));
if (rn && rn >= 0) {
console.log('Remote row index', rowIndex, key);
return rn;
}
}
}
return undefined;
},
getState: (key) => {
return get()[key];
},
@ -702,6 +756,30 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
},
pageSize: 50,
ready: false,
refreshCells: (fromRow?: number, toRow?: number, col?: number) => {
const state = get();
const damageList: { cell: [number, number] }[] = [];
const colLen = Object.keys(state.renderColumns ?? [1, 2, 3]).length;
const from = fromRow && fromRow > 0 ? fromRow : 0;
const to = toRow && toRow >= from ? toRow : from + state.pageSize;
for (let row = from; row <= to; row++) {
if (col && col > 0) {
damageList.push({
cell: [col, row],
});
} else {
for (let c = 0; c <= colLen; c++) {
damageList.push({
cell: [c, row],
});
}
}
}
state._glideref?.updateCells(damageList);
},
setState: (key, value) => {
set(
produce((state) => {
@ -816,60 +894,9 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
};
}, [setState, getState]);
/// logic to apply the selected row.
useEffect(() => {
const ready = getState('ready');
const ref = getState('_glideref');
const keyField = getState('keyField') ?? 'id';
const selectedRow = getState('selectedRow') ?? props.selectedRow;
const askAPIRowNumber = getState('askAPIRowNumber');
let rowIndex = -1;
if (selectedRow && ref && ready) {
const page_data = getState('_page_data');
const pageSize = getState('pageSize');
for (const p in page_data) {
for (const r in page_data[p]) {
const idx = Number(p) * pageSize + Number(r);
//console.log('Found row', idx, page_data[p][r]?.[keyField], selectedRow);
if (String(page_data[p][r]?.[keyField]) === String(selectedRow)) {
rowIndex =
page_data[p][r]?._rownumber > 0 ? page_data[p][r]?._rownumber : idx > 0 ? idx : -1;
break;
}
}
if (rowIndex > 0) {
break;
}
}
if (rowIndex > 0) {
ref.scrollTo(0, rowIndex);
getState('_events').dispatchEvent(
new CustomEvent('selectedRowFound', {
detail: { rowNumber: rowIndex, selectedRow: selectedRow },
})
);
} else if (typeof askAPIRowNumber === 'function') {
askAPIRowNumber(String(selectedRow))
.then((r) => {
if (r >= 0) {
ref.scrollTo(0, r);
getState('_events').dispatchEvent(
new CustomEvent('selectedRowFound', {
detail: { rowNumber: r, selectedRow: selectedRow },
})
);
}
})
.catch((e) => {
console.warn('Error in askAPIRowNumber', e);
});
}
}
}, [props.selectedRow]);
getState('_events').addEventListener('reload', (_e: Event) => {
getState('reload')?.();
getState('refreshCells')?.();
});
return {
@ -877,6 +904,7 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
colSort: props.defaultSort ?? getState('colSort') ?? [],
hideMenu: props.hideMenu ?? menus.hide,
scrollToRowKey: props.scrollToRowKey ?? props.selectedRowKey ?? getState('scrollToRowKey'),
showMenu: props.showMenu ?? menus.show,
total_rows: props.total_rows ?? getState('total_rows') ?? 0,
};

View File

@ -0,0 +1,52 @@
import React, { type PropsWithChildren, type Ref, useImperativeHandle } from 'react';
import { type GridlerRef, useGridlerStore } from './GridlerStore';
function _GridlerRefHandler(props: PropsWithChildren, ref: Ref<GridlerRef> | undefined) {
const [setStateFN, getstate] = useGridlerStore((s) => [s.setStateFN, s.getState]);
useImperativeHandle<GridlerRef, GridlerRef>(ref, () => {
return {
getGlideRef: () => {
return getstate('_glideref');
},
getState: getstate,
refresh: async (parms?: any) => {
const refreshCells = getstate('refreshCells');
const loadPage = getstate('loadPage');
loadPage?.(parms?.pageIndex ?? 0, 'all');
refreshCells?.();
},
reload: async (parms?: any) => {
const refreshCells = getstate('refreshCells');
const loadPage = getstate('loadPage');
loadPage?.(parms?.pageIndex ?? 0, 'all');
refreshCells?.();
},
reloadRow: async (key: number | string) => {
const refreshCells = getstate('refreshCells');
//const loadPage = getstate('loadPage');
const getRowIndexByKey = getstate('getRowIndexByKey');
const rn = await getRowIndexByKey?.(String(key));
if (rn && rn >= 0) {
refreshCells?.(rn, rn + 1);
//todo loadpage or row from server
}
},
scrollToRow: async (key: number | string) => {
if (key && Number(key) >= 0) {
setStateFN('scrollToRowKey', (cv) => Number(key ?? cv));
}
},
selectRow: async (key: number | string) => {
if (key && Number(key) >= 0) {
setStateFN('selectedRowKey', (cv) => Number(key ?? cv));
}
},
setStateFN: setStateFN,
};
}, []);
return <>{props.children}</>;
}
export const GridlerRefHandler = React.forwardRef(_GridlerRefHandler);

View File

@ -56,6 +56,7 @@ function _GlidlerLocalDataAdaptor<T = unknown>(props: GlidlerLocalDataAdaptorPro
setState('total_rows', sortedData.length);
setState('data', sortedData);
refChanged.current.colSort = colSort;
getState('refreshCells')?.();
}
}, [colSort, props.onColumnSort]);
@ -65,6 +66,7 @@ function _GlidlerLocalDataAdaptor<T = unknown>(props: GlidlerLocalDataAdaptorPro
setState('total_rows', filteredData.length);
setState('data', filteredData);
refChanged.current.colFilters = colFilters;
getState('refreshCells')?.();
}
}, [colFilters, props.onColumnFilter]);

View File

@ -102,8 +102,8 @@ export const useGridTheme = () => {
// }[colorScheme];
// for (const selectedRow of gridSelection?.rows) {
// if (selectedRow === row) {
// for (const scrollToRowKey of gridSelection?.rows) {
// if (scrollToRowKey === row) {
// return {
// bgCell: rowColor.bgCell,
// bgCellMedium: rowColor.bgCellMedium

View File

@ -1,10 +1,11 @@
import { Checkbox, Divider, Group, Stack, TagsInput, TextInput } from '@mantine/core';
import { Button, Checkbox, Divider, Group, Stack, TagsInput, TextInput } from '@mantine/core';
import { useLocalStorage } from '@mantine/hooks';
import { useState } from 'react';
import { useRef, useState } from 'react';
import type { GridlerColumns } from '../components/Column';
import { GlidlerAPIAdaptorForGoLangv2 } from '../components/adaptors';
import { type GridlerRef } from '../components/GridlerStore';
import { Gridler } from '../Gridler';
export const GridlerGoAPIExampleEventlog = () => {
@ -12,6 +13,7 @@ export const GridlerGoAPIExampleEventlog = () => {
defaultValue: 'http://localhost:8080/api',
key: 'apiurl',
});
const ref = useRef<GridlerRef>(null);
const [apiKey, setApiKey] = useLocalStorage({ defaultValue: '', key: 'apikey' });
const [selectRow, setSelectRow] = useState<string | undefined>('');
const [values, setValues] = useState<Array<Record<string, any>>>([]);
@ -106,8 +108,9 @@ export const GridlerGoAPIExampleEventlog = () => {
//console.log('GridlerGoAPIExampleEventlog onChange', v);
setValues(v);
}}
ref={ref}
scrollToRowKey={selectRow ? parseInt(selectRow, 10) : undefined}
sections={{ ...sections, rightElementDisabled: false }}
selectedRow={selectRow ? parseInt(selectRow, 10) : undefined}
selectFirstRowOnMount={true}
selectMode="row"
title="Go API Example"
@ -142,6 +145,43 @@ export const GridlerGoAPIExampleEventlog = () => {
/>
;
</Group>
<Group>
<Button
onClick={() => {
ref.current?.refresh();
}}
>
Refresh
</Button>
<Button
onClick={() => {
ref.current?.selectRow(20523);
}}
>
Select 20523
</Button>
<Button
onClick={() => {
ref.current?.selectRow(4);
}}
>
Select 4
</Button>
<Button
onClick={() => {
ref.current?.reloadRow(20523);
}}
>
Reload 20523
</Button>
<Button
onClick={() => {
ref.current?.scrollToRow(16272);
}}
>
Goto 2050
</Button>
</Group>
</Stack>
);
};