bo-grid 0.2.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -5
- package/dist/grid/Cell.svelte +50 -2
- package/dist/grid/Cell.svelte.d.ts +5 -0
- package/dist/grid/FilterMenu.svelte +263 -0
- package/dist/grid/FilterMenu.svelte.d.ts +15 -0
- package/dist/grid/Grid.svelte +581 -31
- package/dist/grid/Grid.svelte.d.ts +33 -1
- package/dist/grid/ToolPanel.svelte +117 -0
- package/dist/grid/ToolPanel.svelte.d.ts +15 -0
- package/dist/grid/column.d.ts +5 -0
- package/dist/grid/column.js +1 -1
- package/dist/grid/filtering.d.ts +41 -0
- package/dist/grid/filtering.js +107 -0
- package/dist/grid/source.d.ts +3 -0
- package/dist/grid/source.js +5 -0
- package/dist/grid/source.svelte.d.ts +2 -1
- package/dist/grid/source.svelte.js +6 -5
- package/dist/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
import type { ColumnDef, GridRow, SortState, CellEditEvent } from './column';
|
|
3
3
|
import { type GridTheme } from './theme';
|
|
4
4
|
import { type AggKind } from './aggregate';
|
|
5
|
+
import { type ColumnFilter } from './filtering';
|
|
5
6
|
import type { RowSource } from './source';
|
|
6
7
|
type $$ComponentProps = {
|
|
7
8
|
rows: GridRow[];
|
|
@@ -21,8 +22,19 @@ type $$ComponentProps = {
|
|
|
21
22
|
/** Called with the selected row ids whenever the row-selection set changes. */
|
|
22
23
|
onRowSelectionChange?: (selectedIds: Array<string | number>) => void;
|
|
23
24
|
/** Column keys to hide (controlled). Build your own column-picker UI and
|
|
24
|
-
drive this prop — the grid stays presentation-only.
|
|
25
|
+
drive this prop — the grid stays presentation-only. Composed (union) with
|
|
26
|
+
columns the user hides at runtime via the column menu. */
|
|
25
27
|
hiddenColumns?: string[];
|
|
28
|
+
/** Called with all currently-hidden column keys whenever the runtime set
|
|
29
|
+
changes (column menu hide/show). */
|
|
30
|
+
onColumnVisibilityChange?: (hidden: string[]) => void;
|
|
31
|
+
/** Enable a per-column header menu (a ⋮ trigger) with sort, hide and (with
|
|
32
|
+
`filterMenu`) filter actions. Default false. */
|
|
33
|
+
columnMenu?: boolean;
|
|
34
|
+
/** Show a "Columns" button that opens a panel to toggle column visibility
|
|
35
|
+
(the place to restore columns hidden via the menu). Lazy-loaded. Default
|
|
36
|
+
false. */
|
|
37
|
+
columnsPanel?: boolean;
|
|
26
38
|
/** Return extra CSS class(es) for a data row (e.g. to colour by value).
|
|
27
39
|
Style them via `:global(.your-class)` since rows live inside the grid. */
|
|
28
40
|
rowClass?: (row: GridRow) => string | undefined;
|
|
@@ -38,6 +50,12 @@ type $$ComponentProps = {
|
|
|
38
50
|
sort?: SortState[];
|
|
39
51
|
/** Called with the new sort order whenever a header is clicked. */
|
|
40
52
|
onSortChange?: (sort: SortState[]) => void;
|
|
53
|
+
/** Controlled column filters (keyed by column key). When set, the grid
|
|
54
|
+
reflects these and reports changes via `onFilterChange` instead of holding
|
|
55
|
+
its own. Omit for uncontrolled filtering. */
|
|
56
|
+
columnFilters?: Record<string, ColumnFilter>;
|
|
57
|
+
/** Called with the full column-filter map whenever a header filter changes. */
|
|
58
|
+
onFilterChange?: (filters: Record<string, ColumnFilter>) => void;
|
|
41
59
|
/** Show a pinned totals row: each column with a `groupAgg` shows that
|
|
42
60
|
aggregate over all (filtered) rows. In-memory mode only. Default false. */
|
|
43
61
|
footer?: boolean;
|
|
@@ -54,6 +72,20 @@ type $$ComponentProps = {
|
|
|
54
72
|
/** Show a per-column filter input row under the header. Rows must match every
|
|
55
73
|
non-empty column filter (AND). In-memory mode only. Default false. */
|
|
56
74
|
filterRow?: boolean;
|
|
75
|
+
/** Enable a per-column header filter menu (lazy-loaded on first open). Each
|
|
76
|
+
filterable column shows a funnel; the menu's control matches the column
|
|
77
|
+
type (text/number/date). Override or disable per column with `col.filter`.
|
|
78
|
+
Works in source mode too (filters are delegated to the `RowSource`); set
|
|
79
|
+
filters need in-memory data. Default false. */
|
|
80
|
+
filterMenu?: boolean;
|
|
81
|
+
/** Show a built-in quick-filter search box above the grid that matches across
|
|
82
|
+
all column values (ANDed with the `filter` prop). In-memory mode only.
|
|
83
|
+
Default false. */
|
|
84
|
+
quickFilter?: boolean;
|
|
85
|
+
/** Show an Excel-style fill handle at the selection's bottom-right corner;
|
|
86
|
+
drag it to copy the selected value(s) across the extended range (editable
|
|
87
|
+
columns only). In-memory mode only. Default false. */
|
|
88
|
+
fillHandle?: boolean;
|
|
57
89
|
/** Message shown when there are no rows. Default 'No matching rows'. */
|
|
58
90
|
emptyMessage?: string;
|
|
59
91
|
/** Show a loading overlay over the grid (for consumer-driven async work in
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Floating columns panel: toggle column visibility (and restore hidden ones).
|
|
3
|
+
// Lazy-loaded by Grid; presentation-only — the parent owns the visibility set.
|
|
4
|
+
let {
|
|
5
|
+
columns,
|
|
6
|
+
hidden,
|
|
7
|
+
x,
|
|
8
|
+
y,
|
|
9
|
+
onToggle,
|
|
10
|
+
onShowAll,
|
|
11
|
+
onClose,
|
|
12
|
+
}: {
|
|
13
|
+
columns: Array<{ key: string; header: string }>;
|
|
14
|
+
hidden: string[];
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
onToggle: (key: string) => void;
|
|
18
|
+
onShowAll: () => void;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
} = $props();
|
|
21
|
+
|
|
22
|
+
let search = $state('');
|
|
23
|
+
const shown = $derived(
|
|
24
|
+
columns.filter((c) => c.header.toLowerCase().includes(search.trim().toLowerCase())),
|
|
25
|
+
);
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<div
|
|
29
|
+
class="bo-toolpanel"
|
|
30
|
+
role="dialog"
|
|
31
|
+
tabindex="-1"
|
|
32
|
+
aria-label="Columns"
|
|
33
|
+
style="left:{x}px;top:{y}px;"
|
|
34
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
35
|
+
onkeydown={(e) => e.key === 'Escape' && onClose()}
|
|
36
|
+
>
|
|
37
|
+
<div class="bo-tp-head">
|
|
38
|
+
<span>Columns</span>
|
|
39
|
+
<button type="button" class="bo-tp-link" onclick={onShowAll}>Show all</button>
|
|
40
|
+
</div>
|
|
41
|
+
<input class="bo-tp-search" type="search" bind:value={search} placeholder="search…" aria-label="Search columns" />
|
|
42
|
+
<div class="bo-tp-list">
|
|
43
|
+
{#each shown as col (col.key)}
|
|
44
|
+
<label class="bo-tp-opt">
|
|
45
|
+
<input type="checkbox" checked={!hidden.includes(col.key)} onchange={() => onToggle(col.key)} />
|
|
46
|
+
<span>{col.header}</span>
|
|
47
|
+
</label>
|
|
48
|
+
{/each}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<style>
|
|
53
|
+
.bo-toolpanel {
|
|
54
|
+
position: fixed;
|
|
55
|
+
z-index: 30;
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
gap: 7px;
|
|
59
|
+
width: 200px;
|
|
60
|
+
padding: 10px;
|
|
61
|
+
background: var(--bo-header-bg);
|
|
62
|
+
border: 0.5px solid var(--bo-border);
|
|
63
|
+
border-radius: 8px;
|
|
64
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
color: var(--bo-text);
|
|
67
|
+
}
|
|
68
|
+
.bo-tp-head {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: baseline;
|
|
71
|
+
justify-content: space-between;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
color: var(--bo-text-dim);
|
|
74
|
+
}
|
|
75
|
+
.bo-tp-link {
|
|
76
|
+
padding: 0;
|
|
77
|
+
font: inherit;
|
|
78
|
+
font-size: 11px;
|
|
79
|
+
color: var(--bo-up);
|
|
80
|
+
background: none;
|
|
81
|
+
border: 0;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
}
|
|
84
|
+
.bo-tp-link:hover {
|
|
85
|
+
text-decoration: underline;
|
|
86
|
+
}
|
|
87
|
+
.bo-tp-search {
|
|
88
|
+
width: 100%;
|
|
89
|
+
padding: 5px 7px;
|
|
90
|
+
font: inherit;
|
|
91
|
+
color: var(--bo-text);
|
|
92
|
+
background: var(--bo-bg);
|
|
93
|
+
border: 0.5px solid var(--bo-border);
|
|
94
|
+
border-radius: 5px;
|
|
95
|
+
}
|
|
96
|
+
.bo-tp-list {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
max-height: 220px;
|
|
100
|
+
overflow-y: auto;
|
|
101
|
+
}
|
|
102
|
+
.bo-tp-opt {
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
gap: 7px;
|
|
106
|
+
padding: 4px 4px;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
}
|
|
110
|
+
.bo-tp-opt:hover {
|
|
111
|
+
background: var(--bo-row-hover);
|
|
112
|
+
}
|
|
113
|
+
.bo-tp-opt span {
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
text-overflow: ellipsis;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
columns: Array<{
|
|
3
|
+
key: string;
|
|
4
|
+
header: string;
|
|
5
|
+
}>;
|
|
6
|
+
hidden: string[];
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
onToggle: (key: string) => void;
|
|
10
|
+
onShowAll: () => void;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
};
|
|
13
|
+
declare const ToolPanel: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
14
|
+
type ToolPanel = ReturnType<typeof ToolPanel>;
|
|
15
|
+
export default ToolPanel;
|
package/dist/grid/column.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Candle } from '../types';
|
|
2
2
|
import { type DateStyle } from '../format/format';
|
|
3
3
|
import type { AggKind } from './aggregate';
|
|
4
|
+
import type { FilterKind } from './filtering';
|
|
4
5
|
export type Align = 'left' | 'right';
|
|
5
6
|
interface ColBase {
|
|
6
7
|
/** Field on the row to read for this column's value. */
|
|
@@ -52,6 +53,10 @@ interface ColBase {
|
|
|
52
53
|
/** Parent header label. Consecutive columns sharing a `group` render under a
|
|
53
54
|
spanning header. Best with fixed-width columns. */
|
|
54
55
|
group?: string;
|
|
56
|
+
/** Header filter-menu control for this column (requires `filterMenu` on
|
|
57
|
+
<Grid>). Defaults to the column's type; `'set'` shows a value checklist;
|
|
58
|
+
`false` disables filtering for this column. */
|
|
59
|
+
filter?: false | FilterKind;
|
|
55
60
|
}
|
|
56
61
|
export interface CellEditEvent {
|
|
57
62
|
row: GridRow;
|
package/dist/grid/column.js
CHANGED
|
@@ -24,7 +24,7 @@ export function isSortable(col) {
|
|
|
24
24
|
return col.type !== 'sparkline' && col.sortable !== false;
|
|
25
25
|
}
|
|
26
26
|
export function isEditable(col) {
|
|
27
|
-
return !!col.editable && col.type !== 'sparkline' && col.type !== '
|
|
27
|
+
return !!col.editable && col.type !== 'sparkline' && col.type !== 'custom';
|
|
28
28
|
}
|
|
29
29
|
function rawCompare(a, b) {
|
|
30
30
|
if (typeof a === 'number' && typeof b === 'number')
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured per-column filter model (v0.3). Pure and dependency-free so it can
|
|
3
|
+
* be unit-tested and reused by both the in-memory `view` and a server source.
|
|
4
|
+
* The header filter-menu UI (lazy-loaded) writes these; `passesFilters` applies
|
|
5
|
+
* them. Filtering is a snapshot operation, not a per-frame one.
|
|
6
|
+
*/
|
|
7
|
+
import type { ColumnDef, GridRow } from './column';
|
|
8
|
+
export type FilterKind = 'text' | 'number' | 'date' | 'set';
|
|
9
|
+
export type TextOp = 'contains' | 'notContains' | 'equals' | 'starts' | 'ends';
|
|
10
|
+
export type NumberOp = 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge' | 'between';
|
|
11
|
+
export type DateOp = 'before' | 'after' | 'on' | 'between';
|
|
12
|
+
export type ColumnFilter = {
|
|
13
|
+
kind: 'text';
|
|
14
|
+
op: TextOp;
|
|
15
|
+
q: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'number';
|
|
18
|
+
op: NumberOp;
|
|
19
|
+
a: number;
|
|
20
|
+
b?: number;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'date';
|
|
23
|
+
op: DateOp;
|
|
24
|
+
a: number;
|
|
25
|
+
b?: number;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'set';
|
|
28
|
+
excluded: string[];
|
|
29
|
+
};
|
|
30
|
+
/** Pick the default filter control for a column from its type. */
|
|
31
|
+
export declare function defaultFilterKind(col: ColumnDef): FilterKind;
|
|
32
|
+
/** A fresh, inactive filter of the given kind (the menu's starting state). */
|
|
33
|
+
export declare function emptyFilter(kind: FilterKind): ColumnFilter;
|
|
34
|
+
/** Whether a filter actually constrains anything (else it's a no-op). */
|
|
35
|
+
export declare function isFilterActive(f: ColumnFilter | undefined | null): boolean;
|
|
36
|
+
/** Does one cell value satisfy one filter? An inactive filter passes everything. */
|
|
37
|
+
export declare function matchesFilter(value: unknown, f: ColumnFilter): boolean;
|
|
38
|
+
/** AND across every active per-column filter. */
|
|
39
|
+
export declare function passesFilters(row: GridRow, filters: Record<string, ColumnFilter>): boolean;
|
|
40
|
+
/** Sorted unique string values for a column — the set-filter checklist. */
|
|
41
|
+
export declare function distinctValues(rows: readonly GridRow[], key: string): string[];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { isNumeric } from './column';
|
|
2
|
+
const DAY = 86_400_000;
|
|
3
|
+
/** Pick the default filter control for a column from its type. */
|
|
4
|
+
export function defaultFilterKind(col) {
|
|
5
|
+
if (col.type === 'date')
|
|
6
|
+
return 'date';
|
|
7
|
+
if (isNumeric(col))
|
|
8
|
+
return 'number';
|
|
9
|
+
return 'text';
|
|
10
|
+
}
|
|
11
|
+
/** A fresh, inactive filter of the given kind (the menu's starting state). */
|
|
12
|
+
export function emptyFilter(kind) {
|
|
13
|
+
switch (kind) {
|
|
14
|
+
case 'number':
|
|
15
|
+
return { kind: 'number', op: 'eq', a: NaN };
|
|
16
|
+
case 'date':
|
|
17
|
+
return { kind: 'date', op: 'on', a: NaN };
|
|
18
|
+
case 'set':
|
|
19
|
+
return { kind: 'set', excluded: [] };
|
|
20
|
+
default:
|
|
21
|
+
return { kind: 'text', op: 'contains', q: '' };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Whether a filter actually constrains anything (else it's a no-op). */
|
|
25
|
+
export function isFilterActive(f) {
|
|
26
|
+
if (!f)
|
|
27
|
+
return false;
|
|
28
|
+
switch (f.kind) {
|
|
29
|
+
case 'text':
|
|
30
|
+
return f.q.trim().length > 0;
|
|
31
|
+
case 'number':
|
|
32
|
+
case 'date':
|
|
33
|
+
return Number.isFinite(f.a) && (f.op !== 'between' || Number.isFinite(f.b));
|
|
34
|
+
case 'set':
|
|
35
|
+
return f.excluded.length > 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Does one cell value satisfy one filter? An inactive filter passes everything. */
|
|
39
|
+
export function matchesFilter(value, f) {
|
|
40
|
+
if (!isFilterActive(f))
|
|
41
|
+
return true;
|
|
42
|
+
switch (f.kind) {
|
|
43
|
+
case 'text': {
|
|
44
|
+
const hay = String(value ?? '').toLowerCase();
|
|
45
|
+
const q = f.q.trim().toLowerCase();
|
|
46
|
+
if (f.op === 'contains')
|
|
47
|
+
return hay.includes(q);
|
|
48
|
+
if (f.op === 'notContains')
|
|
49
|
+
return !hay.includes(q);
|
|
50
|
+
if (f.op === 'equals')
|
|
51
|
+
return hay === q;
|
|
52
|
+
if (f.op === 'starts')
|
|
53
|
+
return hay.startsWith(q);
|
|
54
|
+
return hay.endsWith(q); // 'ends'
|
|
55
|
+
}
|
|
56
|
+
case 'set':
|
|
57
|
+
return !f.excluded.includes(String(value ?? ''));
|
|
58
|
+
case 'number':
|
|
59
|
+
case 'date': {
|
|
60
|
+
// Empty/blank cells aren't numbers (Number(null)/Number('') coerce to 0),
|
|
61
|
+
// so exclude them explicitly while a number/date filter is active.
|
|
62
|
+
if (value === null || value === undefined || value === '')
|
|
63
|
+
return false;
|
|
64
|
+
const n = Number(value);
|
|
65
|
+
if (!Number.isFinite(n))
|
|
66
|
+
return false; // non-numeric is excluded while active
|
|
67
|
+
if (f.kind === 'date' && f.op === 'on') {
|
|
68
|
+
return Math.floor(n / DAY) === Math.floor(f.a / DAY); // same (UTC) day
|
|
69
|
+
}
|
|
70
|
+
switch (f.op) {
|
|
71
|
+
case 'eq':
|
|
72
|
+
return n === f.a;
|
|
73
|
+
case 'ne':
|
|
74
|
+
return n !== f.a;
|
|
75
|
+
case 'lt':
|
|
76
|
+
case 'before':
|
|
77
|
+
return n < f.a;
|
|
78
|
+
case 'le':
|
|
79
|
+
return n <= f.a;
|
|
80
|
+
case 'gt':
|
|
81
|
+
case 'after':
|
|
82
|
+
return n > f.a;
|
|
83
|
+
case 'ge':
|
|
84
|
+
return n >= f.a;
|
|
85
|
+
case 'between':
|
|
86
|
+
return n >= f.a && n <= (f.b ?? Infinity);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return true; // unreachable fallback
|
|
91
|
+
}
|
|
92
|
+
/** AND across every active per-column filter. */
|
|
93
|
+
export function passesFilters(row, filters) {
|
|
94
|
+
for (const key in filters) {
|
|
95
|
+
const f = filters[key];
|
|
96
|
+
if (isFilterActive(f) && !matchesFilter(row[key], f))
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
/** Sorted unique string values for a column — the set-filter checklist. */
|
|
102
|
+
export function distinctValues(rows, key) {
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
for (const row of rows)
|
|
105
|
+
seen.add(String(row[key] ?? ''));
|
|
106
|
+
return [...seen].sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
|
|
107
|
+
}
|
package/dist/grid/source.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GridRow, SortState } from './column';
|
|
2
|
+
import { type ColumnFilter } from './filtering';
|
|
2
3
|
export interface RowRange {
|
|
3
4
|
/** First row index (inclusive). */
|
|
4
5
|
start: number;
|
|
@@ -13,6 +14,8 @@ export interface RowSourceParams {
|
|
|
13
14
|
/** Full sort order (primary first) for multi-column sort. May be empty. */
|
|
14
15
|
sorts?: SortState[];
|
|
15
16
|
filter: string;
|
|
17
|
+
/** Structured per-column filters (header filter menu), keyed by column key. */
|
|
18
|
+
columnFilters?: Record<string, ColumnFilter>;
|
|
16
19
|
}
|
|
17
20
|
export interface RowSourceResult {
|
|
18
21
|
/** Rows for the requested range, in order. */
|
package/dist/grid/source.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { compareBySorts } from './column';
|
|
2
|
+
import { passesFilters } from './filtering';
|
|
2
3
|
/**
|
|
3
4
|
* Adapt an in-memory array to the RowSource interface — applies sort/filter and
|
|
4
5
|
* slices the requested range. Useful as a real client-side adapter and for
|
|
@@ -12,6 +13,10 @@ export function createArraySource(all, opts = {}) {
|
|
|
12
13
|
if (f && filterKeys && filterKeys.length > 0) {
|
|
13
14
|
rows = rows.filter((r) => filterKeys.some((k) => String(r[k] ?? '').toLowerCase().includes(f)));
|
|
14
15
|
}
|
|
16
|
+
const cf = params.columnFilters;
|
|
17
|
+
if (cf && Object.keys(cf).length > 0) {
|
|
18
|
+
rows = rows.filter((r) => passesFilters(r, cf));
|
|
19
|
+
}
|
|
15
20
|
const sorts = params.sorts?.length ? params.sorts : params.sort ? [params.sort] : [];
|
|
16
21
|
if (sorts.length > 0) {
|
|
17
22
|
rows = [...rows].sort((a, b) => compareBySorts(a, b, sorts));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GridRow, SortState } from './column';
|
|
2
2
|
import type { RowRange, RowSource } from './source';
|
|
3
|
+
import type { ColumnFilter } from './filtering';
|
|
3
4
|
/**
|
|
4
5
|
* Drives a RowSource for the grid: fetches the visible window, caches rows by
|
|
5
6
|
* index, tracks the total, and guards against stale responses. Cache is keyed by
|
|
@@ -17,5 +18,5 @@ export declare class RowSourceController {
|
|
|
17
18
|
constructor(source: RowSource);
|
|
18
19
|
rowAt(index: number): GridRow | null;
|
|
19
20
|
private keyOf;
|
|
20
|
-
fetch(range: RowRange, sorts: SortState[], filter: string): Promise<void>;
|
|
21
|
+
fetch(range: RowRange, sorts: SortState[], filter: string, columnFilters?: Record<string, ColumnFilter>): Promise<void>;
|
|
21
22
|
}
|
|
@@ -18,11 +18,12 @@ export class RowSourceController {
|
|
|
18
18
|
rowAt(index) {
|
|
19
19
|
return this.cache.get(index) ?? null;
|
|
20
20
|
}
|
|
21
|
-
keyOf(sorts, filter) {
|
|
22
|
-
|
|
21
|
+
keyOf(sorts, filter, columnFilters) {
|
|
22
|
+
const cf = columnFilters ? JSON.stringify(columnFilters) : '';
|
|
23
|
+
return `${sorts.map((s) => `${s.key}:${s.dir}`).join(',')}|${filter}|${cf}`;
|
|
23
24
|
}
|
|
24
|
-
async fetch(range, sorts, filter) {
|
|
25
|
-
const key = this.keyOf(sorts, filter);
|
|
25
|
+
async fetch(range, sorts, filter, columnFilters) {
|
|
26
|
+
const key = this.keyOf(sorts, filter, columnFilters);
|
|
26
27
|
if (key !== this.key) {
|
|
27
28
|
this.key = key;
|
|
28
29
|
this.cache.clear();
|
|
@@ -40,7 +41,7 @@ export class RowSourceController {
|
|
|
40
41
|
return;
|
|
41
42
|
const id = ++this.reqId;
|
|
42
43
|
this.loading = true;
|
|
43
|
-
const res = await this.source.getRows({ range, sort: sorts[0] ?? null, sorts, filter });
|
|
44
|
+
const res = await this.source.getRows({ range, sort: sorts[0] ?? null, sorts, filter, columnFilters });
|
|
44
45
|
if (id !== this.reqId)
|
|
45
46
|
return; // a newer request superseded this one
|
|
46
47
|
this.total = res.total;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { default as Grid } from './grid/Grid.svelte';
|
|
|
2
2
|
export { default as Sparkline } from './sparkline/Sparkline.svelte';
|
|
3
3
|
export type { ColumnDef, Align, GridRow, SortDir, SortState, CellEditEvent } from './grid/column';
|
|
4
4
|
export type { AggKind, AggResult } from './grid/aggregate';
|
|
5
|
+
export type { ColumnFilter, FilterKind, TextOp, NumberOp, DateOp } from './grid/filtering';
|
|
5
6
|
export { fmtPrice, fmtPercent, fmtVolume, fmtDate } from './format/format';
|
|
6
7
|
export type { DateStyle } from './format/format';
|
|
7
8
|
export { aggregate } from './grid/aggregate';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bo-grid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Tiny, fast Svelte 5 data grid: canvas sparklines, batched realtime cell updates, and virtual scrolling. A free, fintech-focused alternative to heavyweight grids.",
|