A lot of refectoring
This commit is contained in:
862
src/Gridler/components/GridlerStore.tsx
Normal file
862
src/Gridler/components/GridlerStore.tsx
Normal file
@@ -0,0 +1,862 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import {
|
||||
type CellArray,
|
||||
CompactSelection,
|
||||
type DataEditorProps,
|
||||
type DataEditorRef,
|
||||
type EditableGridCell,
|
||||
type GetCellsThunk,
|
||||
type GridCell,
|
||||
GridCellKind,
|
||||
type GridColumn,
|
||||
type GridMouseEventArgs,
|
||||
type GridSelection,
|
||||
type HeaderClickedEventArgs,
|
||||
type Item,
|
||||
type Rectangle,
|
||||
} from '@glideapps/glide-data-grid';
|
||||
import { IconGrid4x4 } from '@tabler/icons-react';
|
||||
import { getUUID } from '@warkypublic/artemis-kit';
|
||||
import { getNestedValue } from '@warkypublic/artemis-kit/object';
|
||||
import { createSyncStore } from '@warkypublic/zustandsyncstore';
|
||||
import { produce } from 'immer';
|
||||
import { type PropsWithChildren, type ReactNode, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
type MantineBetterMenuInstance,
|
||||
type MantineBetterMenuInstanceItem,
|
||||
useMantineBetterMenus,
|
||||
} from '../../MantineBetterMenu';
|
||||
import { ColumnFilterSet, type GridlerColumn, type GridlerColumns } from './Column';
|
||||
import { SortDownSprite } from './sprites/SortDown';
|
||||
import { SortUpSprite } from './sprites/SortUp';
|
||||
import { SpriteImage } from './sprites/SpriteImage';
|
||||
|
||||
export type FilterOption = {
|
||||
datatype?: 'array' | 'boolean' | 'date' | 'function' | 'number' | 'object' | 'string';
|
||||
id: string;
|
||||
operator: FilterOptionOperator;
|
||||
value: string;
|
||||
value2?: string;
|
||||
};
|
||||
export type FilterOptionOperator =
|
||||
| 'between'
|
||||
| 'contains'
|
||||
| 'endswith'
|
||||
| 'eq'
|
||||
| 'gt'
|
||||
| 'gte'
|
||||
| 'lt'
|
||||
| 'lte'
|
||||
| 'neq'
|
||||
| 'startswith';
|
||||
|
||||
export interface GridlerProps extends PropsWithChildren {
|
||||
askAPIRowNumber?: (key: string) => Promise<number>;
|
||||
columns?: GridlerColumns;
|
||||
|
||||
defaultSort?: Array<SortOption>;
|
||||
enableOddEvenRowColor?: boolean;
|
||||
getMenuItems?: (
|
||||
id: string,
|
||||
storeState: GridlerState,
|
||||
row?: any,
|
||||
col?: GridlerColumn,
|
||||
defaultItems?: Array<MantineBetterMenuInstanceItem>
|
||||
) => Array<MantineBetterMenuInstanceItem>;
|
||||
|
||||
glideProps?: Partial<DataEditorProps>;
|
||||
headerHeight?: number;
|
||||
height?: number | string;
|
||||
hideMenu?: (id: string) => void;
|
||||
keyField?: string;
|
||||
maxConcurrency?: number;
|
||||
onChange?: (values: Array<Record<string, any>>) => void;
|
||||
onMounted?: (getState: GridlerState['getState'], setState: GridlerState['setState']) => void;
|
||||
onUnMounted?: () => void;
|
||||
pageSize?: number;
|
||||
progressiveScroll?: boolean;
|
||||
RenderCell?: <TRowType extends Record<string, string>>(
|
||||
row: any,
|
||||
colindex: number,
|
||||
colid: string,
|
||||
value: any,
|
||||
store: GridlerState
|
||||
) => GridCell;
|
||||
|
||||
rowHeight?: number;
|
||||
sections?: {
|
||||
bottom?: React.ReactNode;
|
||||
left?: React.ReactNode;
|
||||
right?: React.ReactNode;
|
||||
rightElementEnd?: React.ReactNode;
|
||||
rightElementStart?: React.ReactNode;
|
||||
top?: React.ReactNode;
|
||||
};
|
||||
selectedRow?: number;
|
||||
selectMode?: 'cell' | 'row';
|
||||
showMenu?: (id: string, options?: Partial<MantineBetterMenuInstance>) => void;
|
||||
tooltipBarProps?: React.HTMLAttributes<HTMLDivElement>;
|
||||
total_rows?: number;
|
||||
uniqueid: string;
|
||||
useAPIQuery?: (index: number) => Promise<Array<Record<string, any>>>;
|
||||
values?: Array<Record<string, any>>;
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
export interface GridlerState {
|
||||
_active_requests?: Array<{ controller: AbortController; page: number }>;
|
||||
_activeTooltip?: ReactNode;
|
||||
_events: EventTarget;
|
||||
_glideref?: DataEditorRef;
|
||||
_gridSelection?: GridSelection;
|
||||
_gridSelectionRows?: GridSelection['rows'];
|
||||
_loadingList: CompactSelection;
|
||||
_page_data: Record<number, Array<any>>;
|
||||
_scrollTimeout?: any | number;
|
||||
_visibleArea: Rectangle;
|
||||
_visiblePages: Rectangle;
|
||||
|
||||
addError: (err: string, ...args: Array<any>) => void;
|
||||
colFilters?: Array<FilterOption>;
|
||||
colOrder?: Record<string, number>;
|
||||
colSize?: Record<string, number>;
|
||||
colSort?: Array<SortOption>;
|
||||
data?: Array<any>;
|
||||
|
||||
errors: Array<string>;
|
||||
focused?: boolean;
|
||||
get: () => GridlerState;
|
||||
getCellContent: (cell: Item) => GridCell;
|
||||
getCellsForSelection: (
|
||||
selection: Rectangle,
|
||||
abortSignal: AbortSignal
|
||||
) => CellArray | GetCellsThunk;
|
||||
getRowBuffer: (row: number) => any;
|
||||
getState: <K extends keyof GridlerStoreState>(key: K) => GridlerStoreState[K];
|
||||
|
||||
hasLocalData: boolean;
|
||||
loadingData?: boolean;
|
||||
loadPage: (page: number, clearMode?: 'all' | 'page') => Promise<void>;
|
||||
mounted: boolean;
|
||||
onCellEdited: (cell: Item, newVal: EditableGridCell) => void;
|
||||
onColumnMoved: (from: number, to: number) => void;
|
||||
onColumnProposeMove: (startIndex: number, endIndex: number) => boolean;
|
||||
onColumnResize: (
|
||||
column: GridColumn,
|
||||
newSize: number,
|
||||
colIndex: number,
|
||||
newSizeWithGrow: number
|
||||
) => void;
|
||||
onContextClick: (area: string, event: any, col?: number, row?: number) => void;
|
||||
onHeaderClicked: (colIndex: number, event: HeaderClickedEventArgs) => void;
|
||||
onHeaderMenuClick: (col: number, screenPosition: Rectangle) => void;
|
||||
onItemHovered: (args: GridMouseEventArgs) => void;
|
||||
|
||||
onVisibleRegionChanged: (
|
||||
r: Rectangle,
|
||||
tx: number,
|
||||
ty: number,
|
||||
extras: {
|
||||
freezeRegion?: Rectangle;
|
||||
freezeRegions?: readonly Rectangle[];
|
||||
selected?: Item;
|
||||
}
|
||||
) => void;
|
||||
|
||||
pageSize: number;
|
||||
reload?: () => Promise<void>;
|
||||
renderColumns?: GridlerColumns;
|
||||
setState: <K extends keyof GridlerStoreState>(
|
||||
key: K,
|
||||
value: Partial<GridlerStoreState[K]>
|
||||
) => void;
|
||||
setStateFN: <K extends keyof GridlerStoreState>(
|
||||
key: K,
|
||||
value: (current: GridlerStoreState[K]) => Partial<GridlerStoreState[K]>
|
||||
) => Promise<void>;
|
||||
toCell: <TRowType extends Record<string, string>>(row: any, col: number) => GridCell;
|
||||
}
|
||||
|
||||
export type GridlerStoreState = GridlerProps & GridlerState;
|
||||
|
||||
export type SortOption = { direction: 'asc' | 'desc'; id: string; order?: number };
|
||||
|
||||
const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreState, GridlerProps>(
|
||||
(set, get) => ({
|
||||
_events: new EventTarget(),
|
||||
_loadingList: CompactSelection.empty(),
|
||||
_page_data: {},
|
||||
_visibleArea: { height: 10000, width: 1000, x: 0, y: 0 },
|
||||
_visiblePages: { height: 0, width: 0, x: 0, y: 0 },
|
||||
addError: (err: string, ...args: Array<any>) => {
|
||||
const s = get();
|
||||
console.log('Gridler Error', s.uniqueid, err, args);
|
||||
set(
|
||||
produce((state: GridlerStoreState) => {
|
||||
state.errors = [...state.errors, err];
|
||||
})
|
||||
);
|
||||
},
|
||||
errors: [],
|
||||
get: () => get(),
|
||||
getCellContent: (cell: Item) => {
|
||||
const state = get();
|
||||
const [col, row] = cell;
|
||||
|
||||
const buffer = state.getRowBuffer(row);
|
||||
|
||||
if (buffer !== undefined) {
|
||||
return state.toCell(buffer, col);
|
||||
}
|
||||
return {
|
||||
allowOverlay: false,
|
||||
kind: GridCellKind.Loading,
|
||||
};
|
||||
},
|
||||
getCellsForSelection: (selection: Rectangle, abortSignal: AbortSignal) => {
|
||||
return async () => {
|
||||
const state = get();
|
||||
//const firstPage = Math.max(0, Math.floor(selection.y / state.pageSize));
|
||||
//const lastPage = Math.floor((selection.y + selection.height) / state.pageSize);
|
||||
|
||||
await state.setStateFN('_visibleArea', (cv) => {
|
||||
//if (r.x === cv.x && r.y === cv.y && r.width === cv.width && r.height === cv.height)
|
||||
// return cv;
|
||||
return selection;
|
||||
});
|
||||
|
||||
const result: GridCell[][] = [];
|
||||
|
||||
for (let y = selection.y; y < selection.y + selection.height; y++) {
|
||||
const row: GridCell[] = [];
|
||||
for (let x = selection.x; x < selection.x + selection.width; x++) {
|
||||
row.push(state.getCellContent([x, y]));
|
||||
}
|
||||
result.push(row);
|
||||
}
|
||||
|
||||
//console.log('Gridler:Debug:getCellsForSelection', selection, result);
|
||||
return result as CellArray;
|
||||
};
|
||||
},
|
||||
getRowBuffer: (row: number) => {
|
||||
const state = get();
|
||||
//Handle local data
|
||||
if (state.data && state.data.length > 0) {
|
||||
if (state.data[row] === undefined) {
|
||||
return {
|
||||
allowOverlay: false,
|
||||
kind: GridCellKind.Loading,
|
||||
};
|
||||
}
|
||||
return state.data[row];
|
||||
}
|
||||
|
||||
//Handle remote paged data
|
||||
const firstPage = Math.max(0, Math.floor(row / state.pageSize));
|
||||
const upperPage = state.pageSize * firstPage;
|
||||
const index = row - upperPage;
|
||||
|
||||
const rowData = state._page_data?.[firstPage]?.[index];
|
||||
|
||||
return rowData;
|
||||
},
|
||||
getState: (key) => {
|
||||
return get()[key];
|
||||
},
|
||||
hasLocalData: false,
|
||||
keyField: 'id',
|
||||
loadPage: async (pPage: number, clearMode?: 'all' | 'page') => {
|
||||
const state = get();
|
||||
const page = pPage < 0 ? 0 : pPage;
|
||||
|
||||
const result = state._events.dispatchEvent(
|
||||
new CustomEvent('before_loadPage', {
|
||||
detail: { clearMode, page: pPage, state },
|
||||
})
|
||||
);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const damageList: { cell: [number, number] }[] = [];
|
||||
const colLen = Object.keys(state.renderColumns ?? [1, 2, 3]).length;
|
||||
const upperPage = state.pageSize * page;
|
||||
const hasPage = state._page_data?.[page]?.length > 0;
|
||||
//console.log('loadPage', page, clearMode, { upperPage, hasPage, pz: state.pageSize, colLen });
|
||||
if (clearMode === 'all') {
|
||||
state._active_requests?.forEach((r) => {
|
||||
r.controller?.abort?.();
|
||||
});
|
||||
|
||||
state.setState('_page_data', {});
|
||||
state.setState('_active_requests', []);
|
||||
|
||||
state.loadPage(page);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.useAPIQuery) {
|
||||
console.warn('No useAPIQuery function defined, cannot load page', page);
|
||||
return;
|
||||
}
|
||||
if (!hasPage && clearMode !== 'page') {
|
||||
state
|
||||
.useAPIQuery?.(page)
|
||||
.then((data) => {
|
||||
state.setStateFN('_page_data', (cv) => {
|
||||
return { ...cv, [page]: data };
|
||||
});
|
||||
for (let row = page; row <= upperPage + state.pageSize; row++) {
|
||||
for (let col = 0; col <= colLen; col++) {
|
||||
damageList.push({
|
||||
cell: [col, row],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
state._glideref?.updateCells(damageList);
|
||||
state._events.dispatchEvent(
|
||||
new CustomEvent('loadPage', {
|
||||
detail: { clearMode, data, page: pPage, state },
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('loadPage Error: ', page, e);
|
||||
state._events.dispatchEvent(
|
||||
new CustomEvent('loadPage_error', {
|
||||
detail: { clearMode, error: e, page: pPage, state },
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
maxConcurrency: 1,
|
||||
mounted: false,
|
||||
onCellEdited: (cell: Item, newVal: EditableGridCell) => {
|
||||
const state = get();
|
||||
const [, row] = cell;
|
||||
//const current = state._editData?.[row];
|
||||
//if (current === undefined) return;
|
||||
//todo: complete
|
||||
state._events.dispatchEvent(
|
||||
new CustomEvent('onCellEdited', {
|
||||
detail: { cell, newVal, row, state },
|
||||
})
|
||||
);
|
||||
},
|
||||
onColumnMoved: (from: number, to: number) => {
|
||||
const s = get();
|
||||
const fromItem = s.renderColumns?.[from];
|
||||
const toItem = s.renderColumns?.[to];
|
||||
if (fromItem?.disableMove || toItem?.disableMove) {
|
||||
return;
|
||||
}
|
||||
|
||||
s.setStateFN('colOrder', (cols) => {
|
||||
const renderCols =
|
||||
cols ??
|
||||
s.renderColumns
|
||||
?.map((col, i) => [col.id, i])
|
||||
.reduce((acc, [id, i]) => ({ ...acc, [id]: i }), {});
|
||||
|
||||
if (!fromItem?.id || !toItem?.id) {
|
||||
return renderCols;
|
||||
}
|
||||
|
||||
return { ...renderCols, [fromItem?.id]: to, [toItem?.id]: from };
|
||||
});
|
||||
},
|
||||
onColumnProposeMove: (startIndex: number, endIndex: number) => {
|
||||
const s = get();
|
||||
const fromItem = s.renderColumns?.[startIndex];
|
||||
const toItem = s.renderColumns?.[endIndex];
|
||||
if (fromItem?.disableMove || toItem?.disableMove) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onColumnResize: (
|
||||
column: GridColumn,
|
||||
newSize: number,
|
||||
colIndex: number,
|
||||
newSizeWithGrow: number
|
||||
) => {
|
||||
const s = get();
|
||||
const col = s.renderColumns?.find((col) => col.id === column.id);
|
||||
if (col?.disableResize) {
|
||||
return;
|
||||
}
|
||||
if (col?.maxWidth && newSize < col?.maxWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (col) {
|
||||
s.setStateFN('colSize', (cols) => {
|
||||
if (cols && col && cols[col.id] === newSize) {
|
||||
return cols;
|
||||
}
|
||||
return { ...cols, [col.id]: newSize };
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onContextClick: (area: string, event: any, col?: number, row?: number) => {
|
||||
const s = get();
|
||||
const coldef = s.renderColumns?.[col ?? -1];
|
||||
|
||||
const items =
|
||||
area === 'menu'
|
||||
? [
|
||||
{
|
||||
label: `Side menu`,
|
||||
},
|
||||
]
|
||||
: coldef
|
||||
? [
|
||||
{
|
||||
items: [
|
||||
{
|
||||
label: 'Sort Ascending',
|
||||
leftSection: <SpriteImage sprite={SortUpSprite} />,
|
||||
onClick: () => {
|
||||
s.setStateFN('colSort', (c) => {
|
||||
const cols = [...(c ?? [])];
|
||||
const idx = cols.findIndex((search) => search.id === coldef.id);
|
||||
const dir = 'asc';
|
||||
if (idx < 0) {
|
||||
const newSort: SortOption = {
|
||||
direction: dir,
|
||||
id: coldef.id,
|
||||
order: cols?.length,
|
||||
};
|
||||
cols.push(newSort);
|
||||
} else if (idx >= 0) {
|
||||
cols[idx].direction = dir;
|
||||
}
|
||||
|
||||
return cols;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Sort Descending',
|
||||
leftSection: <SpriteImage sprite={SortDownSprite} />,
|
||||
onClick: () => {
|
||||
s.setStateFN('colSort', (c) => {
|
||||
const cols = [...(c ?? [])];
|
||||
const idx = cols.findIndex((search) => search.id === coldef.id);
|
||||
const dir = 'desc';
|
||||
if (idx < 0) {
|
||||
const newSort: SortOption = {
|
||||
direction: dir,
|
||||
id: coldef.id,
|
||||
order: cols?.length,
|
||||
};
|
||||
cols.push(newSort);
|
||||
} else if (idx >= 0) {
|
||||
cols[idx].direction = dir;
|
||||
}
|
||||
|
||||
return cols;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Filter ${coldef?.title ?? coldef?.id}`,
|
||||
},
|
||||
{
|
||||
renderer: <ColumnFilterSet column={coldef} storeState={get()} />,
|
||||
},
|
||||
],
|
||||
label: `Column Settings for ${coldef?.title ?? coldef?.id}`,
|
||||
leftSection: <IconGrid4x4 size={16} />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
s.hideMenu?.(area);
|
||||
|
||||
s.showMenu?.(area, {
|
||||
items:
|
||||
coldef?.getMenuItems?.(
|
||||
area,
|
||||
s,
|
||||
col && row ? s.getRowBuffer(row) : undefined,
|
||||
coldef,
|
||||
items
|
||||
) ??
|
||||
s.getMenuItems?.(area, s, col && row ? s.getRowBuffer(row) : undefined, coldef, items) ??
|
||||
items,
|
||||
x: event.clientX ?? event.bounds?.x,
|
||||
y: event.clientY ?? event.bounds?.y,
|
||||
});
|
||||
},
|
||||
onHeaderClicked: (colIndex: number, event: HeaderClickedEventArgs) => {
|
||||
const s = get();
|
||||
event.preventDefault();
|
||||
const col = s.renderColumns?.[colIndex];
|
||||
if (!col) {
|
||||
return;
|
||||
}
|
||||
if (!col.disableSort) {
|
||||
s.setStateFN('colSort', (c) => {
|
||||
const cols = [...(c ?? [])];
|
||||
const idx = cols.findIndex((search) => search.id === col.id);
|
||||
|
||||
if (idx < 0) {
|
||||
const newSort: SortOption = { direction: 'asc', id: col.id, order: cols?.length };
|
||||
cols.push(newSort);
|
||||
} else if (idx >= 0 && cols[idx].direction === 'asc') {
|
||||
cols[idx].direction = 'desc';
|
||||
} else if (idx >= 0 && cols[idx].direction === 'desc') {
|
||||
cols.splice(idx, 1);
|
||||
}
|
||||
|
||||
return cols;
|
||||
});
|
||||
}
|
||||
},
|
||||
onHeaderMenuClick: (col: number, screenPosition: Rectangle) => {
|
||||
const s = get();
|
||||
const coldef = s.renderColumns?.[col];
|
||||
if (!coldef) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sortItems = [
|
||||
{
|
||||
label: `Sort ${coldef?.title ?? coldef?.id}`,
|
||||
},
|
||||
{
|
||||
label: 'Sort Ascending',
|
||||
leftSection: <SpriteImage sprite={SortUpSprite} />,
|
||||
onClick: () => {
|
||||
s.setStateFN('colSort', (c) => {
|
||||
const cols = [...(c ?? [])];
|
||||
const idx = cols.findIndex((search) => search.id === coldef.id);
|
||||
const dir = 'asc';
|
||||
if (idx < 0) {
|
||||
const newSort: SortOption = { direction: dir, id: coldef.id, order: cols?.length };
|
||||
cols.push(newSort);
|
||||
} else if (idx >= 0) {
|
||||
cols[idx].direction = dir;
|
||||
}
|
||||
|
||||
return cols;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Sort Descending',
|
||||
leftSection: <SpriteImage sprite={SortDownSprite} />,
|
||||
onClick: () => {
|
||||
s.setStateFN('colSort', (c) => {
|
||||
const cols = [...(c ?? [])];
|
||||
const idx = cols.findIndex((search) => search.id === coldef.id);
|
||||
const dir = 'desc';
|
||||
if (idx < 0) {
|
||||
const newSort: SortOption = { direction: dir, id: coldef.id, order: cols?.length };
|
||||
cols.push(newSort);
|
||||
} else if (idx >= 0) {
|
||||
cols[idx].direction = dir;
|
||||
}
|
||||
|
||||
return cols;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
isDivider: true,
|
||||
},
|
||||
];
|
||||
const items = [
|
||||
...(coldef.disableSort ? [] : sortItems),
|
||||
{
|
||||
label: `Filter ${coldef?.title ?? coldef?.id}`,
|
||||
},
|
||||
{
|
||||
renderer: <ColumnFilterSet column={coldef} storeState={get()} />,
|
||||
},
|
||||
{
|
||||
isDivider: true,
|
||||
},
|
||||
{
|
||||
label: `Refresh`,
|
||||
onClickAsync: async () => {
|
||||
await s.reload?.();
|
||||
},
|
||||
},
|
||||
];
|
||||
s.hideMenu?.('header-menu');
|
||||
s.showMenu?.('header-menu', {
|
||||
items:
|
||||
coldef?.getMenuItems?.('header-menu', s, undefined, coldef, items) ??
|
||||
s.getMenuItems?.('header-menu', s, undefined, coldef, items),
|
||||
x: screenPosition.x,
|
||||
y: screenPosition.y,
|
||||
});
|
||||
},
|
||||
onItemHovered: (args: GridMouseEventArgs) => {
|
||||
const s = get();
|
||||
s.setState('_activeTooltip', undefined);
|
||||
if (args.kind === 'cell') {
|
||||
//_activeTooltip
|
||||
const coldef = s.renderColumns?.[args.location[0]];
|
||||
if (coldef?.tooltip && typeof coldef?.tooltip === 'string') {
|
||||
s.setState('_activeTooltip', coldef?.tooltip);
|
||||
} else if (coldef?.tooltip && typeof coldef?.tooltip === 'function') {
|
||||
const buffer = s.getRowBuffer(args.location[1]);
|
||||
s.setState('_activeTooltip', coldef?.tooltip(buffer, args.location[1], args.location[0]));
|
||||
}
|
||||
}
|
||||
},
|
||||
onVisibleRegionChanged: (
|
||||
region: Rectangle,
|
||||
tx: number,
|
||||
ty: number,
|
||||
extras: {
|
||||
freezeRegion?: Rectangle;
|
||||
freezeRegions?: readonly Rectangle[];
|
||||
selected?: Item;
|
||||
}
|
||||
) => {
|
||||
const state = get();
|
||||
if (state._scrollTimeout) {
|
||||
clearTimeout(state._scrollTimeout);
|
||||
}
|
||||
|
||||
const onVisibleRegionChangedDebounced = () => {
|
||||
//console.log('Gridler:Debug:VisibleRegionChanged', r);
|
||||
const state = get();
|
||||
const firstPage = Math.max(0, Math.floor(region.y / state.pageSize));
|
||||
const lastPage = Math.floor((region.y + region.height) / state.pageSize);
|
||||
const upperPage = state.pageSize * firstPage;
|
||||
|
||||
const previousPage = Math.max(0, Math.floor(state._visiblePages.y / state.pageSize));
|
||||
const pageDif = firstPage - previousPage;
|
||||
// console.log(
|
||||
// 'Page dif',
|
||||
// pageDif,
|
||||
// { previousPage, firstPage, lastPage, onder: pageDif - 1 === lastPage },
|
||||
// region
|
||||
// );
|
||||
// if (pageDif - 1 === lastPage) {
|
||||
// console.log('Is onder', lastPage, pageDif);
|
||||
// }
|
||||
|
||||
if (state.progressiveScroll && pageDif > 1) {
|
||||
const state = get();
|
||||
const newpos = (previousPage + 2) * state.pageSize * (state.rowHeight ?? 22);
|
||||
// console.log('Pending scroll exec', newpos, {
|
||||
// pageSize: state.pageSize,
|
||||
// rowHeight: state.rowHeight
|
||||
// });
|
||||
state._glideref?.scrollTo(
|
||||
0,
|
||||
{
|
||||
amount: newpos,
|
||||
unit: 'px',
|
||||
},
|
||||
'vertical'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
//console.log('VIsi change', { firstPage, lastPage }, state?._page_data[lastPage]);
|
||||
// if (state._active_requests && state._active_requests.filter((f)=>f.page > firstPage - 1)) {
|
||||
|
||||
// }
|
||||
|
||||
//console.log('Gridler:Debug:VisibleRegionChanged', region, firstPage, lastPage);
|
||||
state.setState('_visiblePages', region);
|
||||
};
|
||||
|
||||
state.setState(
|
||||
'_scrollTimeout',
|
||||
setTimeout(() => {
|
||||
onVisibleRegionChangedDebounced();
|
||||
}, 100)
|
||||
);
|
||||
},
|
||||
|
||||
pageSize: 50,
|
||||
setState: (key, value) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
state[key] = value;
|
||||
})
|
||||
);
|
||||
},
|
||||
setStateFN: (key, value) => {
|
||||
const p = new Promise<void>((resolve, reject) => {
|
||||
set(
|
||||
produce((state) => {
|
||||
if (typeof value === 'function') {
|
||||
state[key] = (value as (value: any) => any)(state[key]);
|
||||
} else {
|
||||
reject(new Error(`Not a function ${value}`));
|
||||
throw Error(`Not a function ${value}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
|
||||
return p;
|
||||
},
|
||||
toCell: <T extends Record<string, string>>(row: T, col: number): GridCell => {
|
||||
const s = get();
|
||||
const columns = s.renderColumns;
|
||||
const coldef: GridlerColumn | undefined = columns?.[col];
|
||||
const ref: string = coldef?.id ?? coldef?.title ?? String(col);
|
||||
|
||||
if (ref === undefined || ref === null || row === undefined || row === null) {
|
||||
if (coldef?.Cell) {
|
||||
return coldef?.Cell(row, col, ref, undefined, s) as GridCell;
|
||||
}
|
||||
if (s.RenderCell) {
|
||||
return s.RenderCell(row, col, ref, undefined, s);
|
||||
}
|
||||
|
||||
return {
|
||||
allowOverlay: false,
|
||||
kind: GridCellKind.Loading,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const val = String(ref).includes('.') ? (getNestedValue(ref, row) ?? '') : row?.[ref];
|
||||
|
||||
if (coldef?.Cell) {
|
||||
return coldef?.Cell(row, col, ref, val, s) as GridCell;
|
||||
}
|
||||
if (s.RenderCell) {
|
||||
return s.RenderCell(row, col, ref, val, s);
|
||||
}
|
||||
|
||||
return {
|
||||
allowOverlay: true,
|
||||
data: val,
|
||||
displayData: String(val),
|
||||
kind: GridCellKind.Text,
|
||||
};
|
||||
} catch (e) {
|
||||
if (s.RenderCell) {
|
||||
return s.RenderCell(row, col, ref, row?.[ref ?? ''], s);
|
||||
}
|
||||
|
||||
return {
|
||||
allowOverlay: false,
|
||||
kind: GridCellKind.Loading,
|
||||
skeletonWidthVariability: 50,
|
||||
};
|
||||
}
|
||||
},
|
||||
total_rows: 1000,
|
||||
uniqueid: getUUID(),
|
||||
}),
|
||||
(props) => {
|
||||
const [setState, getState] = props.useStore((s) => [s.setState, s.getState]);
|
||||
|
||||
const menus = useMantineBetterMenus();
|
||||
|
||||
useEffect(() => {
|
||||
const onMounted = getState('onMounted');
|
||||
if (typeof onMounted === 'function') {
|
||||
onMounted(getState, setState);
|
||||
}
|
||||
setState('mounted', true);
|
||||
if (window && window.document) {
|
||||
const portalElement = window.document.getElementById('portal');
|
||||
if (!portalElement) {
|
||||
const div = window.document.createElement('div');
|
||||
div.id = 'portal';
|
||||
div.setAttribute('data-gridler-portal', props.uniqueid);
|
||||
window.document.body.appendChild(div);
|
||||
}
|
||||
}
|
||||
getState('_events').dispatchEvent(
|
||||
new CustomEvent('mounted', {
|
||||
detail: {},
|
||||
})
|
||||
);
|
||||
return () => {
|
||||
const onUnMounted = getState('onUnMounted');
|
||||
setState('mounted', false);
|
||||
getState('_events').dispatchEvent(
|
||||
new CustomEvent('unmounted', {
|
||||
detail: {},
|
||||
})
|
||||
);
|
||||
if (typeof onUnMounted === 'function') {
|
||||
onUnMounted();
|
||||
}
|
||||
};
|
||||
}, [setState, getState]);
|
||||
|
||||
/// logic to apply the selected row.
|
||||
useEffect(() => {
|
||||
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) {
|
||||
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);
|
||||
} 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')?.();
|
||||
});
|
||||
|
||||
return {
|
||||
...props,
|
||||
|
||||
hideMenu: props.hideMenu ?? menus.hide,
|
||||
showMenu: props.showMenu ?? menus.show,
|
||||
total_rows: props.total_rows ?? getState('total_rows') ?? 0,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export type useStoreReturnType = ReturnType<typeof useGridlerStore>;
|
||||
|
||||
export { Provider, useGridlerStore };
|
||||
Reference in New Issue
Block a user