|
|
|
@ -105,7 +105,7 @@ export type ColumnFiltersState = ColumnFilterEntry[];
|
|
|
|
export interface ResolvedColumn<TData = unknown> {
|
|
|
|
export interface ResolvedColumn<TData = unknown> {
|
|
|
|
id: string;
|
|
|
|
id: string;
|
|
|
|
def: ColumnDef<TData>;
|
|
|
|
def: ColumnDef<TData>;
|
|
|
|
getValue(row: TData): unknown;
|
|
|
|
getValue(row: TData, index: number): unknown;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function readAccessorKey(row: unknown, key: string): unknown {
|
|
|
|
function readAccessorKey(row: unknown, key: string): unknown {
|
|
|
|
@ -117,7 +117,7 @@ function resolveColumns<TData>(defs: ColumnDef<TData>[]): ResolvedColumn<TData>[
|
|
|
|
return defs.map((def, idx) => {
|
|
|
|
return defs.map((def, idx) => {
|
|
|
|
const id = def.id ?? def.accessorKey ?? `col_${idx}`;
|
|
|
|
const id = def.id ?? def.accessorKey ?? `col_${idx}`;
|
|
|
|
const getValue = def.accessorFn
|
|
|
|
const getValue = def.accessorFn
|
|
|
|
? (row: TData) => def.accessorFn!(row, 0)
|
|
|
|
? (row: TData, index: number) => def.accessorFn!(row, index)
|
|
|
|
: def.accessorKey
|
|
|
|
: def.accessorKey
|
|
|
|
? (row: TData) => readAccessorKey(row, def.accessorKey!)
|
|
|
|
? (row: TData) => readAccessorKey(row, def.accessorKey!)
|
|
|
|
: () => undefined;
|
|
|
|
: () => undefined;
|
|
|
|
@ -125,6 +125,11 @@ function resolveColumns<TData>(defs: ColumnDef<TData>[]): ResolvedColumn<TData>[
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface IndexedRow<TData> {
|
|
|
|
|
|
|
|
data: TData;
|
|
|
|
|
|
|
|
index: number;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Sort logic ───────────────────────────────────────────────────────────────
|
|
|
|
// ── Sort logic ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function defaultSortFn(a: unknown, b: unknown): number {
|
|
|
|
function defaultSortFn(a: unknown, b: unknown): number {
|
|
|
|
@ -137,10 +142,10 @@ function defaultSortFn(a: unknown, b: unknown): number {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applySorting<TData>(
|
|
|
|
function applySorting<TData>(
|
|
|
|
rows: TData[],
|
|
|
|
rows: IndexedRow<TData>[],
|
|
|
|
sorting: SortingState,
|
|
|
|
sorting: SortingState,
|
|
|
|
columns: ResolvedColumn<TData>[]
|
|
|
|
columns: ResolvedColumn<TData>[]
|
|
|
|
): TData[] {
|
|
|
|
): IndexedRow<TData>[] {
|
|
|
|
if (sorting.length === 0) return rows;
|
|
|
|
if (sorting.length === 0) return rows;
|
|
|
|
|
|
|
|
|
|
|
|
const sorted = [...rows];
|
|
|
|
const sorted = [...rows];
|
|
|
|
@ -149,7 +154,7 @@ function applySorting<TData>(
|
|
|
|
const col = columns.find((c) => c.id === entry.id);
|
|
|
|
const col = columns.find((c) => c.id === entry.id);
|
|
|
|
if (!col) continue;
|
|
|
|
if (!col) continue;
|
|
|
|
const sortFn = col.def.sortingFn ?? defaultSortFn;
|
|
|
|
const sortFn = col.def.sortingFn ?? defaultSortFn;
|
|
|
|
const result = sortFn(col.getValue(a), col.getValue(b));
|
|
|
|
const result = sortFn(col.getValue(a.data, a.index), col.getValue(b.data, b.index));
|
|
|
|
if (result !== 0) return entry.desc ? -result : result;
|
|
|
|
if (result !== 0) return entry.desc ? -result : result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
return 0;
|
|
|
|
@ -178,36 +183,36 @@ export type BuiltInFilterFn = keyof typeof filterFns;
|
|
|
|
// ── Filter logic ─────────────────────────────────────────────────────────
|
|
|
|
// ── Filter logic ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function applyColumnFilters<TData>(
|
|
|
|
function applyColumnFilters<TData>(
|
|
|
|
rows: TData[],
|
|
|
|
rows: IndexedRow<TData>[],
|
|
|
|
filters: ColumnFiltersState,
|
|
|
|
filters: ColumnFiltersState,
|
|
|
|
columns: ResolvedColumn<TData>[]
|
|
|
|
columns: ResolvedColumn<TData>[]
|
|
|
|
): TData[] {
|
|
|
|
): IndexedRow<TData>[] {
|
|
|
|
if (filters.length === 0) return rows;
|
|
|
|
if (filters.length === 0) return rows;
|
|
|
|
|
|
|
|
|
|
|
|
return rows.filter((row) =>
|
|
|
|
return rows.filter((row) =>
|
|
|
|
filters.every((filter) => {
|
|
|
|
filters.every((filter) => {
|
|
|
|
const col = columns.find((c) => c.id === filter.id);
|
|
|
|
const col = columns.find((c) => c.id === filter.id);
|
|
|
|
if (!col) return true;
|
|
|
|
if (!col) return true;
|
|
|
|
const cellValue = col.getValue(row);
|
|
|
|
const cellValue = col.getValue(row.data, row.index);
|
|
|
|
if (col.def.filterFn) return col.def.filterFn(row, col.id, filter.value);
|
|
|
|
if (col.def.filterFn) return col.def.filterFn(row.data, col.id, filter.value);
|
|
|
|
return filterFns.includesString(cellValue, filter.value);
|
|
|
|
return filterFns.includesString(cellValue, filter.value);
|
|
|
|
})
|
|
|
|
})
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyGlobalFilter<TData>(
|
|
|
|
function applyGlobalFilter<TData>(
|
|
|
|
rows: TData[],
|
|
|
|
rows: IndexedRow<TData>[],
|
|
|
|
globalFilter: string,
|
|
|
|
globalFilter: string,
|
|
|
|
columns: ResolvedColumn<TData>[],
|
|
|
|
columns: ResolvedColumn<TData>[],
|
|
|
|
globalFilterFn?: (row: TData, columnId: string, filterValue: unknown) => boolean
|
|
|
|
globalFilterFn?: (row: TData, columnId: string, filterValue: unknown) => boolean
|
|
|
|
): TData[] {
|
|
|
|
): IndexedRow<TData>[] {
|
|
|
|
if (!globalFilter) return rows;
|
|
|
|
if (!globalFilter) return rows;
|
|
|
|
|
|
|
|
|
|
|
|
return rows.filter((row) =>
|
|
|
|
return rows.filter((row) =>
|
|
|
|
columns.some((col) => {
|
|
|
|
columns.some((col) => {
|
|
|
|
if (col.def.enableFiltering !== true) return false;
|
|
|
|
if (col.def.enableFiltering !== true) return false;
|
|
|
|
const cellValue = col.getValue(row);
|
|
|
|
const cellValue = col.getValue(row.data, row.index);
|
|
|
|
if (globalFilterFn) return globalFilterFn(row, col.id, globalFilter);
|
|
|
|
if (globalFilterFn) return globalFilterFn(row.data, col.id, globalFilter);
|
|
|
|
return filterFns.includesString(cellValue, globalFilter);
|
|
|
|
return filterFns.includesString(cellValue, globalFilter);
|
|
|
|
})
|
|
|
|
})
|
|
|
|
);
|
|
|
|
);
|
|
|
|
@ -215,7 +220,10 @@ function applyGlobalFilter<TData>(
|
|
|
|
|
|
|
|
|
|
|
|
// ── Pagination logic ─────────────────────────────────────────────────────────
|
|
|
|
// ── Pagination logic ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function applyPagination<TData>(rows: TData[], pagination: PaginationState | undefined): TData[] {
|
|
|
|
function applyPagination<TData>(
|
|
|
|
|
|
|
|
rows: IndexedRow<TData>[],
|
|
|
|
|
|
|
|
pagination: PaginationState | undefined
|
|
|
|
|
|
|
|
): IndexedRow<TData>[] {
|
|
|
|
if (!pagination) return rows;
|
|
|
|
if (!pagination) return rows;
|
|
|
|
const start = pagination.pageIndex * pagination.pageSize;
|
|
|
|
const start = pagination.pageIndex * pagination.pageSize;
|
|
|
|
return rows.slice(start, start + pagination.pageSize);
|
|
|
|
return rows.slice(start, start + pagination.pageSize);
|
|
|
|
@ -434,10 +442,11 @@ export function createTable<TData>(config: TableConfig<TData>): TableInstance<TD
|
|
|
|
// ── Derived: sorted + paginated rows ─────────────────────────────────
|
|
|
|
// ── Derived: sorted + paginated rows ─────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const allRows = $derived.by(() => config.data());
|
|
|
|
const allRows = $derived.by(() => config.data());
|
|
|
|
|
|
|
|
const indexedRows = $derived.by(() => allRows.map((data, index) => ({ data, index })));
|
|
|
|
|
|
|
|
|
|
|
|
const filteredRows = $derived.by(() => {
|
|
|
|
const filteredRows = $derived.by(() => {
|
|
|
|
if (!config.filtering?.enabled || config.filtering?.manual) return allRows;
|
|
|
|
if (!config.filtering?.enabled || config.filtering?.manual) return indexedRows;
|
|
|
|
let rows = applyColumnFilters(allRows, columnFilters, resolvedColumns);
|
|
|
|
let rows = applyColumnFilters(indexedRows, columnFilters, resolvedColumns);
|
|
|
|
rows = applyGlobalFilter(rows, globalFilter, resolvedColumns, config.filtering?.globalFilterFn);
|
|
|
|
rows = applyGlobalFilter(rows, globalFilter, resolvedColumns, config.filtering?.globalFilterFn);
|
|
|
|
return rows;
|
|
|
|
return rows;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
@ -479,30 +488,23 @@ export function createTable<TData>(config: TableConfig<TData>): TableInstance<TD
|
|
|
|
)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
function buildRow(data: TData, globalIdx: number, rowIdx: number): TableRow<TData> {
|
|
|
|
function buildRow(row: IndexedRow<TData>, rowIdx: number): TableRow<TData> {
|
|
|
|
const id = getRowId(data, globalIdx);
|
|
|
|
const id = getRowId(row.data, row.index);
|
|
|
|
const orderedCols = getOrderedColumns();
|
|
|
|
const orderedCols = getOrderedColumns();
|
|
|
|
const cells: TableCell<TData>[] = orderedCols.map((col) => ({
|
|
|
|
const cells: TableCell<TData>[] = orderedCols.map((col) => ({
|
|
|
|
id: `${id}_${col.id}`,
|
|
|
|
id: `${id}_${col.id}`,
|
|
|
|
column: col,
|
|
|
|
column: col,
|
|
|
|
value: col.getValue(data)
|
|
|
|
value: col.getValue(row.data, row.index)
|
|
|
|
}));
|
|
|
|
}));
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
id,
|
|
|
|
id,
|
|
|
|
index: rowIdx,
|
|
|
|
index: rowIdx,
|
|
|
|
original: data,
|
|
|
|
original: row.data,
|
|
|
|
cells
|
|
|
|
cells
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const rows = $derived.by(() =>
|
|
|
|
const rows = $derived.by(() => paginatedRows.map((row, idx) => buildRow(row, idx)));
|
|
|
|
paginatedRows.map((data, idx) => {
|
|
|
|
|
|
|
|
const globalIdx = config.pagination?.enabled
|
|
|
|
|
|
|
|
? pagination.pageIndex * pagination.pageSize + idx
|
|
|
|
|
|
|
|
: idx;
|
|
|
|
|
|
|
|
return buildRow(data, globalIdx, idx);
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const rowsById = $derived.by(() => {
|
|
|
|
const rowsById = $derived.by(() => {
|
|
|
|
const map = new Map<string, TableRow<TData>>();
|
|
|
|
const map = new Map<string, TableRow<TData>>();
|
|
|
|
@ -675,8 +677,8 @@ export function createTable<TData>(config: TableConfig<TData>): TableInstance<TD
|
|
|
|
selection = {};
|
|
|
|
selection = {};
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
const next: RowSelectionState = {};
|
|
|
|
const next: RowSelectionState = {};
|
|
|
|
sortedRows.forEach((row, idx) => {
|
|
|
|
sortedRows.forEach((row) => {
|
|
|
|
next[getRowId(row, idx)] = true;
|
|
|
|
next[getRowId(row.data, row.index)] = true;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
selection = next;
|
|
|
|
selection = next;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -689,7 +691,7 @@ export function createTable<TData>(config: TableConfig<TData>): TableInstance<TD
|
|
|
|
|
|
|
|
|
|
|
|
function getIsAllSelected(): boolean {
|
|
|
|
function getIsAllSelected(): boolean {
|
|
|
|
if (sortedRows.length === 0) return false;
|
|
|
|
if (sortedRows.length === 0) return false;
|
|
|
|
return sortedRows.every((row, idx) => selection[getRowId(row, idx)]);
|
|
|
|
return sortedRows.every((row) => selection[getRowId(row.data, row.index)]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getIsSomeSelected(): boolean {
|
|
|
|
function getIsSomeSelected(): boolean {
|
|
|
|
@ -698,7 +700,8 @@ export function createTable<TData>(config: TableConfig<TData>): TableInstance<TD
|
|
|
|
if (sortedRows.length === 0) return false;
|
|
|
|
if (sortedRows.length === 0) return false;
|
|
|
|
let visibleSelected = 0;
|
|
|
|
let visibleSelected = 0;
|
|
|
|
for (let idx = 0; idx < sortedRows.length; idx++) {
|
|
|
|
for (let idx = 0; idx < sortedRows.length; idx++) {
|
|
|
|
if (selection[getRowId(sortedRows[idx], idx)]) visibleSelected++;
|
|
|
|
const row = sortedRows[idx];
|
|
|
|
|
|
|
|
if (selection[getRowId(row.data, row.index)]) visibleSelected++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return visibleSelected > 0 && visibleSelected < sortedRows.length;
|
|
|
|
return visibleSelected > 0 && visibleSelected < sortedRows.length;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|